diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index 7024d150cfb..f7c21e42564 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -38,7 +38,6 @@ import ( "k8s.io/client-go/rest" - corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" "github.com/go-logr/logr" @@ -117,9 +116,9 @@ func (r *AccountReconciler) syncAccount(ctx context.Context, owner string, userN if err := r.syncResourceQuotaAndLimitRange(ctx, userNamespace); err != nil { r.Logger.Error(err, "sync resource resourceQuota and limitRange failed") } - if err := r.adaptEphemeralStorageLimitRange(ctx, userNamespace); err != nil { - r.Logger.Error(err, "adapt ephemeral storage limitRange failed") - } + //if err := r.adaptEphemeralStorageLimitRange(ctx, userNamespace); err != nil { + // r.Logger.Error(err, "adapt ephemeral storage limitRange failed") + //} if getUsername(userNamespace) != owner { return nil, nil } @@ -146,22 +145,22 @@ func (r *AccountReconciler) syncResourceQuotaAndLimitRange(ctx context.Context, return nil } -func (r *AccountReconciler) adaptEphemeralStorageLimitRange(ctx context.Context, nsName string) error { - limit := resources.GetDefaultLimitRange(nsName, nsName) - return retry.Retry(10, 1*time.Second, func() error { - _, err := controllerutil.CreateOrUpdate(ctx, r.Client, limit, func() error { - if len(limit.Spec.Limits) == 0 { - limit = resources.GetDefaultLimitRange(nsName, nsName) - } - limit.Spec.Limits[0].DefaultRequest[corev1.ResourceEphemeralStorage] = resources.LimitRangeDefault[corev1.ResourceEphemeralStorage] - limit.Spec.Limits[0].Default[corev1.ResourceEphemeralStorage] = resources.LimitRangeDefault[corev1.ResourceEphemeralStorage] - //if _, ok := limit.Spec.Limits[0].Default[corev1.ResourceEphemeralStorage]; !ok { - //} - return nil - }) - return err - }) -} +//func (r *AccountReconciler) adaptEphemeralStorageLimitRange(ctx context.Context, nsName string) error { +// limit := resources.GetDefaultLimitRange(nsName, nsName) +// return retry.Retry(10, 1*time.Second, func() error { +// _, err := controllerutil.CreateOrUpdate(ctx, r.Client, limit, func() error { +// if len(limit.Spec.Limits) == 0 { +// limit = resources.GetDefaultLimitRange(nsName, nsName) +// } +// limit.Spec.Limits[0].DefaultRequest[corev1.ResourceEphemeralStorage] = resources.LimitRangeDefault[corev1.ResourceEphemeralStorage] +// limit.Spec.Limits[0].Default[corev1.ResourceEphemeralStorage] = resources.LimitRangeDefault[corev1.ResourceEphemeralStorage] +// //if _, ok := limit.Spec.Limits[0].Default[corev1.ResourceEphemeralStorage]; !ok { +// //} +// return nil +// }) +// return err +// }) +//} // SetupWithManager sets up the controller with the Manager. func (r *AccountReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { diff --git a/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl b/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl index 29001bbfbbe..d050afa43b6 100644 --- a/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/objectstorage/deploy/manifests/deploy.yaml.tmpl @@ -276,6 +276,31 @@ rules: - get - patch - update + - apiGroups: + - '' + resources: + - resourcequotas + verbs: + - get + - patch + - update + - create + - delete + - watch + - list + - apiGroups: + - '' + resources: + - resourcequotas/status + verbs: + - get + - patch + - update + - create + - delete + - watch + - list + --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/controllers/pkg/resources/resources.go b/controllers/pkg/resources/resources.go index 4452acd08e9..542798b12be 100644 --- a/controllers/pkg/resources/resources.go +++ b/controllers/pkg/resources/resources.go @@ -405,6 +405,14 @@ const ( QuotaLimitsNodePorts = "QUOTA_LIMITS_NODE_PORTS" QuotaObjectStorageSize = "QUOTA_OBJECT_STORAGE_SIZE" QuotaObjectStorageBucket = "QUOTA_OBJECT_STORAGE_BUCKET" + + LimitRangeCPU = "LIMIT_RANGE_CPU" + LimitRangeMemory = "LIMIT_RANGE_MEMORY" + LimitRangeEphemeralStorage = "LIMIT_RANGE_EPHEMERAL_STORAGE" + + LimitRangeRepCPU = "LIMIT_RANGE_REP_CPU" + LimitRangeRepMemory = "LIMIT_RANGE_REP_MEMORY" + LimitRangeRepEphemeralStorage = "LIMIT_RANGE_REP_EPHEMERAL_STORAGE" ) const ( @@ -436,14 +444,40 @@ func DefaultLimitRangeLimits() []corev1.LimitRangeItem { return []corev1.LimitRangeItem{ { Type: corev1.LimitTypeContainer, - Default: LimitRangeDefault, - DefaultRequest: LimitRangeDefault, + Default: defaultLimitRange, + DefaultRequest: defaultLimitRangeReq, }, } } -var LimitRangeDefault = corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("50m"), - corev1.ResourceMemory: resource.MustParse("64Mi"), - corev1.ResourceEphemeralStorage: resource.MustParse("100Mi"), +var defaultLimitRange, defaultLimitRangeReq = getLimitRangeDefault(), getLimitRangeReq() + +func getLimitRangeDefault() corev1.ResourceList { + rcList := corev1.ResourceList{} + cpu, memory, ephemeralStorage := resource.MustParse(env.GetEnvWithDefault(LimitRangeCPU, "50m")), resource.MustParse(env.GetEnvWithDefault(LimitRangeMemory, "64Mi")), resource.MustParse(env.GetEnvWithDefault(LimitRangeEphemeralStorage, "100Mi")) + if !cpu.IsZero() { + rcList[corev1.ResourceCPU] = cpu + } + if !memory.IsZero() { + rcList[corev1.ResourceMemory] = memory + } + if !ephemeralStorage.IsZero() { + rcList[corev1.ResourceEphemeralStorage] = ephemeralStorage + } + return rcList +} + +func getLimitRangeReq() corev1.ResourceList { + rcList := corev1.ResourceList{} + cpu, memory, ephemeralStorage := resource.MustParse(env.GetEnvWithDefault(LimitRangeRepCPU, "50m")), resource.MustParse(env.GetEnvWithDefault(LimitRangeRepMemory, "64Mi")), resource.MustParse(env.GetEnvWithDefault(LimitRangeRepEphemeralStorage, "100Mi")) + if !cpu.IsZero() { + rcList[corev1.ResourceCPU] = cpu + } + if !memory.IsZero() { + rcList[corev1.ResourceMemory] = memory + } + if !ephemeralStorage.IsZero() { + rcList[corev1.ResourceEphemeralStorage] = ephemeralStorage + } + return rcList } diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md b/docs/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md index 45a286ef0f8..4389f959b97 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md @@ -31,7 +31,7 @@ description: 了解Sealos系统应用的基本原理与实现,包括Kubernetes 组成:前端系统+Kubernetes API -用户管理 Kbernetes 原生 CronJob 资源,逻辑与应用管理基本相同。 +用户管理 Kubernetes 原生 CronJob 资源,逻辑与应用管理基本相同。 ## 数据库 diff --git a/docs/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md b/docs/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md index 44145e3422b..a88e636c53a 100644 --- a/docs/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md +++ b/docs/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md @@ -167,7 +167,7 @@ docker build -t java-demo . 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: ``` - docker push damager6666/java-demo:v2 + docker push damager6666/demo:v2 ``` ## 步骤7:登陆 Sealos @@ -277,4 +277,4 @@ https://tmgkflgdlstl.cloud.sealos.io/getPersons ![](images/java-example-13.png) -- 页面上显示之间插入到数据库的数据 ,表示你的 Java 应用程序已经在 Sealos 上运行 \ No newline at end of file +- 页面上显示之间插入到数据库的数据 ,表示你的 Java 应用程序已经在 Sealos 上运行 diff --git a/extensions/ide/vscode/devbox/CHANGELOG.md b/extensions/ide/vscode/devbox/CHANGELOG.md index 7806372741a..28ca02e9af3 100644 --- a/extensions/ide/vscode/devbox/CHANGELOG.md +++ b/extensions/ide/vscode/devbox/CHANGELOG.md @@ -6,6 +6,14 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +## [1.3.0] - 2024-12-20 + +## [1.2.2] - 2024-12-9 + +### Fixed + +- Fix the refresh bug of devbox list view. + ## [1.2.1] - 2024-12-4 ### Fixed diff --git a/extensions/ide/vscode/devbox/l10n/bundle.l10n.zh-CN.json b/extensions/ide/vscode/devbox/l10n/bundle.l10n.zh-CN.json index 77a7081b935..07d546cb711 100644 --- a/extensions/ide/vscode/devbox/l10n/bundle.l10n.zh-CN.json +++ b/extensions/ide/vscode/devbox/l10n/bundle.l10n.zh-CN.json @@ -7,7 +7,7 @@ "Copy Password": "复制密码", "Copy Connection String": "复制连接串", "Connection string copied to clipboard!": "连接串已复制到剪贴板!", - "Please select a region,RegionList are added by your each connection.": "请选择一个可用区,可用区来自于您的每个连接。", + "Please select a region,every region are from your each connection.": "请选择一个可用区,可用区来自于您的每个连接。", "Only Devbox can be opened.": "只能打开 Devbox。", "Are you sure to delete?": "确定删除Devbox?", "This action will only delete the devbox ssh config in the local environment.": "此操作只会删除本地环境中的 Devbox SSH 配置。", @@ -19,6 +19,11 @@ "Open in Browser": "在浏览器中打开", "Preview in Editor": "在编辑器中预览", "Open Database Web Terminal": "打开数据库 Web 终端", - "Cursor's Devbox is often not the latest. If there are any issues, please manually install the [plugin](https://marketplace.visualstudio.com/items?itemName=labring.devbox-aio&ssr=false#overview) referenced this [URI](https://www.cursor.com/how-to-install-extension).": "Cursor 的 Devbox 通常不是最新的。如果有任何问题,请手动安装 [插件](https://marketplace.visualstudio.com/items?itemName=labring.devbox-aio&ssr=false#overview) 参考此 [URI](https://www.cursor.com/how-to-install-extension).", - "SSH Port is not correct,maybe your devbox's nodeport is over the limit": "SSH 端口不正确,可能您的 Devbox 的 nodeport 超过了限制" + "The Devbox of Cursor is usually not the latest. If there are any issues, please manually install the [plugin](https://marketplace.visualstudio.com/items?itemName=labring.devbox-aio&ssr=false#overview) referring to this [URI](https://www.cursor.com/how-to-install-extension).": "Cursor 的 Devbox 通常不是最新的。如果有任何问题,请手动安装 [插件](https://marketplace.visualstudio.com/items?itemName=labring.devbox-aio&ssr=false#overview) 参考此 [URI](https://www.cursor.com/how-to-install-extension).", + "SSH Port is not correct,maybe your devbox's nodeport is over the limit": "SSH 端口不正确,可能您的 Devbox 的 nodeport 超过了限制", + "Failed to write SSH configuration": "写入 SSH 配置失败", + "Failed to write SSH private key": "写入 SSH 私钥失败", + "Please install \"Remote - SSH\" extension to connect to a devbox workspace.": "请安装 \"Remote - SSH\" 扩展以连接到 Devbox 工作区。", + "Install": "安装", + "Cancel": "取消" } diff --git a/extensions/ide/vscode/devbox/package.json b/extensions/ide/vscode/devbox/package.json index e384d5e51ad..5329d79ee14 100644 --- a/extensions/ide/vscode/devbox/package.json +++ b/extensions/ide/vscode/devbox/package.json @@ -2,7 +2,7 @@ "name": "devbox-aio", "displayName": "%displayName%", "description": "%description%", - "version": "1.2.1", + "version": "1.3.0", "keywords": [ "devbox", "remote development", @@ -206,9 +206,6 @@ ] } }, - "extensionDependencies": [ - "ms-vscode-remote.remote-ssh" - ], "scripts": { "vscode:prepublish": "npm run package", "compile": "webpack", diff --git a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts index abe61265603..82000f5e4e5 100644 --- a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts +++ b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts @@ -16,6 +16,16 @@ import { GlobalStateManager } from '../utils/globalStateManager' import { ensureFileAccessPermission, ensureFileExists } from '../utils/file' import { modifiedRemoteSSHConfig } from '../utils/remoteSSHConfig' +const message = { + FailedToWriteSSHConfig: vscode.l10n.t('Failed to write SSH configuration'), + FailedToWriteSSHPrivateKey: vscode.l10n.t('Failed to write SSH private key'), + PleaseInstallRemoteSSH: vscode.l10n.t( + 'Please install "Remote - SSH" extension to connect to a devbox workspace.' + ), + Install: vscode.l10n.t('Install'), + Cancel: vscode.l10n.t('Cancel'), +} + export class RemoteSSHConnector extends Disposable { constructor(context: vscode.ExtensionContext) { super() @@ -126,7 +136,7 @@ export class RemoteSSHConnector extends Disposable { }) { Logger.info(`Connecting to remote SSH: ${args.sshHostLabel}`) - this.ensureRemoteSSHExtInstalled() + await this.ensureRemoteSSHExtInstalled() const { sshDomain, sshPort, base64PrivateKey, sshHostLabel, workingDir } = args @@ -203,7 +213,7 @@ export class RemoteSSHConnector extends Disposable { } catch (error) { Logger.error(`Failed to write SSH configuration: ${error}`) vscode.window.showErrorMessage( - `Failed to write SSH configuration: ${error}` + `${message.FailedToWriteSSHConfig}: ${error}` ) } @@ -217,7 +227,7 @@ export class RemoteSSHConnector extends Disposable { } catch (error) { Logger.error(`Failed to write SSH private key: ${error}`) vscode.window.showErrorMessage( - `Failed to write SSH private key: ${error}` + `${message.FailedToWriteSSHPrivateKey}: ${error}` ) } @@ -257,11 +267,11 @@ export class RemoteSSHConnector extends Disposable { return true } - const install = 'Install' - const cancel = 'Cancel' + const install = message.Install + const cancel = message.Cancel const action = await vscode.window.showInformationMessage( - 'Please install "Remote - SSH" extension to connect to a Gitpod workspace.', + message.PleaseInstallRemoteSSH, { modal: true }, install, cancel @@ -271,10 +281,6 @@ export class RemoteSSHConnector extends Disposable { return false } - vscode.window.showInformationMessage( - 'Installing "ms-vscode-remote.remote-ssh" extension' - ) - await vscode.commands.executeCommand( 'extension.open', 'ms-vscode-remote.remote-ssh' diff --git a/extensions/ide/vscode/devbox/src/extension.ts b/extensions/ide/vscode/devbox/src/extension.ts index d512c644974..6aba1304866 100644 --- a/extensions/ide/vscode/devbox/src/extension.ts +++ b/extensions/ide/vscode/devbox/src/extension.ts @@ -37,7 +37,7 @@ export async function activate(context: vscode.ExtensionContext) { const remoteUri = workspaceFolder.uri.authority const devboxId = remoteUri.replace(/^ssh-remote\+/, '') // devbox = sshHostLabel const region = GlobalStateManager.getRegion(devboxId) - updateBaseUrl(`http://devbox.${region}`) + updateBaseUrl(`https://devbox.${region}`) } // network view diff --git a/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts index f003132f2fd..74d22e52292 100644 --- a/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts +++ b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts @@ -26,6 +26,9 @@ const messages = { feedbackInHelpDesk: vscode.l10n.t( 'Give us a feedback in our help desk system.' ), + devboxDeleted: vscode.l10n.t( + 'This Devbox has been deleted in the cloud.Now it cannot be opened. Do you want to delete its local ssh configuration?' + ), } export class DevboxListViewProvider extends Disposable { @@ -50,14 +53,21 @@ export class DevboxListViewProvider extends Disposable { this._register( devboxDashboardView.onDidChangeVisibility(() => { if (devboxDashboardView.visible) { - projectTreeDataProvider.refresh() + projectTreeDataProvider.refreshData() } }) ) // commands + this._register( + devboxDashboardView.onDidChangeVisibility(() => { + if (devboxDashboardView.visible) { + projectTreeDataProvider.refreshData() + } + }) + ) this._register( vscode.commands.registerCommand('devboxDashboard.refresh', () => { - projectTreeDataProvider.refresh() + projectTreeDataProvider.refreshData() }) ) this._register( @@ -106,17 +116,9 @@ class ProjectTreeDataProvider if (fs.existsSync(defaultDevboxSSHConfigPath)) { convertSSHConfigToVersion2(defaultDevboxSSHConfigPath) } - this.refreshData() - setInterval(() => { - this.refresh() - }, 3 * 1000) } - refresh(): void { - this.refreshData() - } - - private async refreshData(): Promise { + async refreshData(): Promise { const data = (await parseSSHConfig( defaultDevboxSSHConfigPath )) as DevboxListItem[] @@ -150,20 +152,13 @@ class ProjectTreeDataProvider item.iconPath = new vscode.ThemeIcon('question') } } catch (error) { - console.error(`get devbox detail failed: ${error}`) - // if ( - // error.toString().includes('500:secrets') && - // error.toString().includes('not found') - // ) { - // const hostParts = item.host.split('_') - // const devboxName = hostParts.slice(2).join('_') - // if (error.toString().includes(devboxName)) { - // await this.delete(item.host, devboxName, true) - - // return - // } - // } - item.iconPath = new vscode.ThemeIcon('warning') + Logger.error(`get devbox detail failed: ${error}`) + if ( + error.toString().includes('500:secrets') && + error.toString().includes('not found') + ) { + item.iconPath = new vscode.ThemeIcon('warning') + } } }) ) @@ -197,6 +192,22 @@ class ProjectTreeDataProvider return } + if ( + item.iconPath instanceof vscode.ThemeIcon && + item.iconPath.id === 'warning' + ) { + const result = await vscode.window.showWarningMessage( + messages.devboxDeleted, + { modal: true }, + 'Yes', + 'No' + ) + if (result === 'Yes') { + await this.delete(item.host, item.label as string, true) + } + return + } + vscode.commands.executeCommand( 'vscode.openFolder', vscode.Uri.parse( @@ -294,7 +305,7 @@ class ProjectTreeDataProvider // TODO: delete known_host public key - this.refresh() + this.refreshData() } catch (error) { vscode.window.showErrorMessage( `${messages.deleteDevboxFailed}: ${error.message}` diff --git a/frontend/desktop/prisma/global/migrations/20241210040952_update_user_tasktype/migration.sql b/frontend/desktop/prisma/global/migrations/20241210040952_update_user_tasktype/migration.sql new file mode 100644 index 00000000000..948a17bf8a5 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20241210040952_update_user_tasktype/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +ALTER TYPE "TaskType" ADD VALUE 'CRONJOB'; +ALTER TYPE "TaskType" ADD VALUE 'DEVBOX'; +ALTER TYPE "TaskType" ADD VALUE 'CONTACT'; +ALTER TYPE "TaskType" ADD VALUE 'REAL_NAME_AUTH'; diff --git a/frontend/desktop/prisma/global/schema.prisma b/frontend/desktop/prisma/global/schema.prisma index b5cca6a11b4..f4353038c04 100644 --- a/frontend/desktop/prisma/global/schema.prisma +++ b/frontend/desktop/prisma/global/schema.prisma @@ -380,6 +380,10 @@ enum TaskType { DATABASE DESKTOP APPSTORE + CRONJOB + DEVBOX + CONTACT + REAL_NAME_AUTH } enum TaskStatus { diff --git a/frontend/desktop/public/images/customer-service-qr.png b/frontend/desktop/public/images/customer-service-qr.png new file mode 100644 index 00000000000..1dc5bd824df Binary files /dev/null and b/frontend/desktop/public/images/customer-service-qr.png differ diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 6009fb2c576..49352373b0a 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -87,8 +87,11 @@ "github": "Github", "google": "Google", "guide_applaunchpad": "Quickly deploy applications without cumbersome configuration", + "guide_costcenter": "Billing standards and resource consumption detailed dashboard", "guide_dbprovider": "Create multiple databases in seconds to meet different application needs", + "guide_devbox": "Automated development environment setup and seamless connection with local IDE", "guide_objectstorage": "Massive storage space, almost bare metal speed experience", + "guide_workorder": "Technical question consultation portal", "handle": "Handle", "have_read": "Have read", "healthy_pod": "Healthy Pods: {{count}}", @@ -211,6 +214,7 @@ "rename": "Rename", "retry_get_qr_code": "reacquire", "scan_qr_code_for_face_recognition": "Use WeChat on your mobile phone to scan the QR code to complete identity verification", + "scan_to_join_group": "Scan the QR code to add exclusive customer service", "scan_with_wechat": "Scan with WeChat", "sealos_copilot": "Sealos Copilot", "sealos_document": "Sealos Document", @@ -268,4 +272,4 @@ "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", "you_have_not_purchased_the_license": "You have not purchased the License", "yuan": "Yuan" -} +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index 97cf3d82a85..a21880bd604 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -84,8 +84,11 @@ "github": "Github", "google": "Google", "guide_applaunchpad": "快速部署应用,无需繁琐配置", + "guide_costcenter": "计费标准、资源消耗明细看板", "guide_dbprovider": "多种数据库秒级创建,满足不同应用需求", + "guide_devbox": "自动化开发环境设置、与本地 IDE 无缝连接", "guide_objectstorage": "海量存储空间,近乎裸机的速度体验", + "guide_workorder": "技术问题咨询入口", "handle": "操作", "have_read": "已读", "healthy_pod": "健康 Pods: {{count}}", @@ -207,6 +210,7 @@ "rename": "重命名", "retry_get_qr_code": "重新获取", "scan_qr_code_for_face_recognition": "使用手机微信扫描二维码,完成身份验证", + "scan_to_join_group": "扫码添加专属客服", "scan_with_wechat": "微信扫码支付", "sealos_copilot": "Sealos 小助理", "sealos_newcomer_benefits": "Sealos 新手福利", @@ -261,4 +265,4 @@ "you_can_view_fees_through_the_fee_center": "您可通过费用中心查看费用", "you_have_not_purchased_the_license": "您还没有购买 License", "yuan": "元" -} \ No newline at end of file +} diff --git a/frontend/desktop/src/components/account/RealNameModal.tsx b/frontend/desktop/src/components/account/RealNameModal.tsx index ba31a37503b..dfc40a332ad 100644 --- a/frontend/desktop/src/components/account/RealNameModal.tsx +++ b/frontend/desktop/src/components/account/RealNameModal.tsx @@ -26,7 +26,14 @@ import { } from '@chakra-ui/react'; import { CloseIcon, useMessage, WarningIcon } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; -import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import React, { + forwardRef, + ReactElement, + useCallback, + useEffect, + useImperativeHandle, + useState +} from 'react'; import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -140,12 +147,15 @@ export function useRealNameAuthNotification(props?: UseToastOptions) { }; } -function RealNameModal(props: { - children: React.ReactElement; - onModalOpen?: () => void; - onModalClose?: () => void; - onFormSuccess?: () => void; -}): ReactElement { +const RealNameModal = forwardRef< + { onOpen: () => void }, + { + children?: React.ReactElement; + onModalOpen?: () => void; + onModalClose?: () => void; + onFormSuccess?: () => void; + } +>(function RealNameModal(props, ref) { const { t } = useTranslation(); const { children } = props; const { isOpen, onOpen, onClose } = useDisclosure(); @@ -157,6 +167,10 @@ function RealNameModal(props: { } }; + useImperativeHandle(ref, () => ({ + onOpen + })); + return ( <> {children && @@ -245,7 +259,7 @@ function RealNameModal(props: { ); -} +}); export function RealNameAuthForm( props: FlexProps & { diff --git a/frontend/desktop/src/components/desktop_content/index.tsx b/frontend/desktop/src/components/desktop_content/index.tsx index 1544265a304..345a330c6e8 100644 --- a/frontend/desktop/src/components/desktop_content/index.tsx +++ b/frontend/desktop/src/components/desktop_content/index.tsx @@ -302,13 +302,6 @@ export default function Desktop(props: any) { }} /> )} - {taskComponentState === 'button' && ( - { - setTaskComponentState('modal'); - }} - /> - )} )} diff --git a/frontend/desktop/src/components/desktop_content/serviceButton.tsx b/frontend/desktop/src/components/desktop_content/serviceButton.tsx index 50589b14acb..905a52a7de3 100644 --- a/frontend/desktop/src/components/desktop_content/serviceButton.tsx +++ b/frontend/desktop/src/components/desktop_content/serviceButton.tsx @@ -1,10 +1,123 @@ import { useConfigStore } from '@/stores/config'; import { useDesktopConfigStore } from '@/stores/desktopConfig'; -import { Box, Center, Flex, Icon, Text } from '@chakra-ui/react'; +import { Box, Center, Divider, Flex, Icon, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; +const OnlineServiceIcon = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +const StarIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; + const OnlineServiceButton = () => { - const { isServiceButtonOpen, setServiceButtonOpen } = useDesktopConfigStore(); + const { isServiceButtonOpen, setServiceButtonOpen, taskComponentState, setTaskComponentState } = + useDesktopConfigStore(); const { t } = useTranslation(); const { layoutConfig } = useConfigStore(); @@ -12,7 +125,8 @@ const OnlineServiceButton = () => { { background="linear-gradient(139deg, rgba(255, 255, 255, 0.80) 0%, rgba(255, 255, 255, 0.70) 100%)" boxShadow="0px 12px 32px -4px rgba(0, 23, 86, 0.20)" backdropFilter="blur(200px)" - cursor="pointer" + cursor={'pointer'} onClick={(e) => { - if (!isServiceButtonOpen) { - setServiceButtonOpen(true); - } else { - window.open(layoutConfig?.customerServiceURL ?? '', '_blank'); - } + e.stopPropagation(); + setServiceButtonOpen(!isServiceButtonOpen); }} > {isServiceButtonOpen ? ( - - - - - + { + if (!isServiceButtonOpen) { + setServiceButtonOpen(true); + } else { + window.open(layoutConfig?.customerServiceURL ?? '', '_blank'); + } + }} + cursor={'pointer'} + > + + - - - - - + + {taskComponentState !== 'none' && } + {taskComponentState !== 'none' && ( + { + setTaskComponentState('modal'); + }} + flexDirection={'column'} + alignItems={'center'} + justifyContent={'center'} + borderRadius={'4px'} + _hover={{ + bg: 'rgba(17, 24, 36, 0.05)' + }} > - - - - - - + + + {t('common:newuser_benefit')} + + + )} + ) : ( { )} - - {isServiceButtonOpen && ( - - {t('online_service')} - - )} ); diff --git a/frontend/desktop/src/components/task/taskModal.tsx b/frontend/desktop/src/components/task/taskModal.tsx index b78c95413c4..898f6f25694 100644 --- a/frontend/desktop/src/components/task/taskModal.tsx +++ b/frontend/desktop/src/components/task/taskModal.tsx @@ -1,5 +1,4 @@ import { IdeaIcon, RightArrowIcon } from '@/components/icons'; -import { I18nCommonKey } from '@/types/i18next'; import { UserTask } from '@/types/task'; import { formatMoney } from '@/utils/format'; import { @@ -9,13 +8,17 @@ import { HStack, Image, Modal, + ModalCloseButton, ModalContent, + ModalHeader, ModalOverlay, Text, + useDisclosure, VStack } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -import React from 'react'; +import React, { useRef } from 'react'; +import RealNameModal from '../account/RealNameModal'; interface TaskModalProps { isOpen: boolean; @@ -26,6 +29,8 @@ interface TaskModalProps { const TaskModal: React.FC = ({ isOpen, onClose, tasks, onTaskClick }) => { const { t, i18n } = useTranslation(); + const { isOpen: isQRCodeOpen, onOpen: onQRCodeOpen, onClose: onQRCodeClose } = useDisclosure(); + const realNameModalRef = useRef<{ onOpen: () => void }>(null); const boxStyles = { border: '1px solid rgba(60, 101, 172, 0.08)', @@ -37,120 +42,169 @@ const TaskModal: React.FC = ({ isOpen, onClose, tasks, onTaskCli }; return ( - - - - background - - - - {t('common:sealos_newcomer_benefits')} - - - {tasks.map((task) => ( - onTaskClick(task)}> - - - - - {task?.title?.[i18n.language]} - - - - - {t('common:balance')} +{formatMoney(Number(task.reward) || 0)} - + <> + + + + background + + + + {t('common:sealos_newcomer_benefits')} + + + {tasks.map((task) => ( + { + if (task.taskType === 'CONTACT') { + onQRCodeOpen(); + } else if (task.taskType === 'REAL_NAME_AUTH') { + realNameModalRef.current?.onOpen(); + } else { + onTaskClick(task); + } + }} + > + + + + + {task?.title?.[i18n.language]} + + - {task.isCompleted ? ( - {t('common:completed')} + {t('common:balance')} +{formatMoney(Number(task.reward) || 0)} - ) : ( - - - {t('common:start_now')} + + {task.isCompleted ? ( + + {t('common:completed')} - - - )} - - - ))} - + ) : ( + + + {t('common:start_now')} + + + + )} + + + ))} + - - - + + + + + + + + + + + + + + {t('common:scan_to_join_group')} + + + + + QR Code + + + {t('common:scan_to_join_group')} + - - - + + + + ); }; diff --git a/frontend/desktop/src/components/task/useDriver.tsx b/frontend/desktop/src/components/task/useDriver.tsx index dafaf0bf0c8..b1896717941 100644 --- a/frontend/desktop/src/components/task/useDriver.tsx +++ b/frontend/desktop/src/components/task/useDriver.tsx @@ -1,5 +1,6 @@ import { checkUserTask, getUserTasks, updateTask } from '@/api/platform'; import { AppStoreIcon, DBproviderIcon, DriverStarIcon, LaunchpadIcon } from '@/components/icons'; +import useAppStore from '@/stores/app'; import { useConfigStore } from '@/stores/config'; import { useDesktopConfigStore } from '@/stores/desktopConfig'; import { UserTask } from '@/types/task'; @@ -20,12 +21,14 @@ export default function useDriver() { const conf = useConfigStore().commonConfig; const { taskComponentState, setTaskComponentState } = useDesktopConfigStore(); const { canShowGuide } = useDesktopConfigStore(); + const { installedApps } = useAppStore(); useEffect(() => { const fetchUserTasks = async () => { await checkUserTask(); const data = await getUserTasks(); - setTasks(data.data); + const filteredTasks = data.data.filter((task) => task.isNewUserTask); + setTasks(filteredTasks); }; fetchUserTasks(); }, [taskComponentState]); @@ -33,21 +36,18 @@ export default function useDriver() { useEffect(() => { const handleUserGuide = async () => { const data = await getUserTasks(); - setTasks(data.data); - const desktopTask = data.data.find((task) => task.taskType === 'DESKTOP'); - const allTasksCompleted = data.data.every((task) => task.isCompleted); + const filteredTasks = data.data.filter((task) => task.isNewUserTask); + setTasks(filteredTasks); + const desktopTask = filteredTasks.find((task) => task.taskType === 'DESKTOP'); + const allTasksCompleted = filteredTasks.every((task) => task.isCompleted); if (!desktopTask?.isCompleted && desktopTask?.id) { - // setTaskComponentState('none'); - // setDesktopGuide(true); setTaskComponentState('none'); - setDesktopGuide(false); // Hide First Guides driverObj.drive(); } else if (allTasksCompleted) { setTaskComponentState('none'); } else { - setTaskComponentState('none'); // Hide task modal for all users - // setTaskComponentState(taskComponentState !== 'none' ? taskComponentState : 'button'); + setTaskComponentState(taskComponentState !== 'none' ? taskComponentState : 'button'); } }; @@ -66,7 +66,7 @@ export default function useDriver() { const desktopTask = tasks.find((task) => task.taskType === 'DESKTOP'); if (desktopTask) { await updateTask(desktopTask.id); - // setTaskComponentState('modal'); // disable task modal for all users + setTaskComponentState('modal'); } } catch (error) {} }; @@ -120,6 +120,7 @@ export default function useDriver() { isShowButtons: false, allowKeyboardControl: false, disableActiveInteraction: true, + // @ts-ignore steps: [ { element: '.apps-container', @@ -160,6 +161,23 @@ export default function useDriver() { ) } }, + { + element: '.system-devbox', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_devbox')} + + + + ) + } + }, { element: '.system-dbprovider', popover: { @@ -210,8 +228,46 @@ export default function useDriver() { ) } + }, + { + element: '.system-costcenter', + popover: { + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', + PopoverBody: ( + + + + {t('common:guide_costcenter')} + + + + ) + } + }, + { + element: '.system-workorder', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_workorder')} + + + + ) + } } - ], + ].filter((step) => { + if (step.element === '.apps-container') return true; + const appKey = step.element.substring(1); + return installedApps.some((app) => app.key === appKey); + }), onDestroyed: () => { completeGuide(); } diff --git a/frontend/desktop/src/pages/api/account/checkTask.ts b/frontend/desktop/src/pages/api/account/checkTask.ts index fc854ce5026..5a5c67ccc49 100644 --- a/frontend/desktop/src/pages/api/account/checkTask.ts +++ b/frontend/desktop/src/pages/api/account/checkTask.ts @@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) include: { task: true } }); - const [deployments, statefulsets, instances, clusters] = await Promise.all([ + const [deployments, statefulsets, instances, clusters, devboxes] = await Promise.all([ k8sApp.listNamespacedDeployment( namespace, undefined, @@ -64,6 +64,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) undefined, undefined, `!${templateDeployKey}` + ) as any, + k8sCustomObjects.listNamespacedCustomObject( + 'devbox.sealos.io', + 'v1alpha1', + namespace, + 'devboxes' ) as any ]); @@ -75,6 +81,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return instances.body.items.length > 0; case TaskType.DATABASE: return clusters.body.items.length > 0; + case TaskType.DEVBOX: + return devboxes.body.items.length > 0; default: return false; } diff --git a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts index 07379944dc1..a91e0aec8c9 100644 --- a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts +++ b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts @@ -195,6 +195,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); + await globalPrisma.userTask.updateMany({ + where: { + userUid, + task: { + taskType: 'REAL_NAME_AUTH' + }, + status: 'NOT_COMPLETED' + }, + data: { + rewardStatus: 'COMPLETED', + status: 'COMPLETED', + completedAt: new Date() + } + }); + return { account: updatedAccount, transaction: accountTransaction, diff --git a/frontend/desktop/src/pages/api/account/getTasks.ts b/frontend/desktop/src/pages/api/account/getTasks.ts index 0dab32e57cb..ef80c1486cc 100644 --- a/frontend/desktop/src/pages/api/account/getTasks.ts +++ b/frontend/desktop/src/pages/api/account/getTasks.ts @@ -27,13 +27,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const tasks = userTasks.map((ut) => ({ id: ut.task.id, - title: JSON.parse(ut.task.title), + title: typeof ut.task.title === 'string' ? JSON.parse(ut.task.title) : ut.task.title, description: ut.task.description, reward: ut.task.reward.toString(), order: ut.task.order, taskType: ut.task.taskType, isCompleted: ut.status === 'COMPLETED', - completedAt: ut.completedAt + completedAt: ut.completedAt, + isNewUserTask: ut.task.isNewUserTask })); const allTasksCompleted = tasks.every((task) => task.isCompleted); @@ -44,6 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) message: allTasksCompleted ? 'All tasks completed' : 'Tasks fetched' }); } catch (error) { + console.log(error); return jsonRes(res, { code: 500, message: 'error' }); } } diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index d746e259823..93790424131 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -137,7 +137,6 @@ async function checkDeductionBalanceAndCreateTasks(userUid: string) { async function createNewUserTasks(tx: TransactionClient, userUid: string) { const newUserTasks = await tx.task.findMany({ where: { - isNewUserTask: true, isActive: true } }); diff --git a/frontend/desktop/src/types/task.ts b/frontend/desktop/src/types/task.ts index b0d1b04f02d..f84a4988ef5 100644 --- a/frontend/desktop/src/types/task.ts +++ b/frontend/desktop/src/types/task.ts @@ -9,4 +9,5 @@ export type UserTask = { taskType: TaskType; isCompleted: boolean; completedAt: string; + isNewUserTask: boolean; }; diff --git a/frontend/providers/applaunchpad/data/config.yaml b/frontend/providers/applaunchpad/data/config.yaml index f7ab8b485c6..998a3f0d05e 100644 --- a/frontend/providers/applaunchpad/data/config.yaml +++ b/frontend/providers/applaunchpad/data/config.yaml @@ -8,7 +8,9 @@ cloud: common: guideEnabled: false apiEnabled: false + gpuEnabled: false launchpad: + pvcStorageMax: 100 eventAnalyze: enabled: false fastGPTKey: "" diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts index 0b899ff4348..0162c5488ff 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts @@ -17,6 +17,8 @@ export type Response = { fileMangerConfig: FileMangerType; SEALOS_USER_DOMAINS: { name: string; secretName: string }[]; DESKTOP_DOMAIN: string; + PVC_STORAGE_MAX: number; + GPU_ENABLED: boolean; }; export const defaultAppConfig: AppConfigType = { @@ -38,6 +40,7 @@ export const defaultAppConfig: AppConfigType = { }, launchpad: { currencySymbol: Coin.shellCoin, + pvcStorageMax: 20, eventAnalyze: { enabled: false, fastGPTKey: '' @@ -80,6 +83,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.log(res); global.AppConfig = res; const gpuNodes = await getGpuNode(); + console.log(gpuNodes, 'gpuNodes'); global.AppConfig.common.gpuEnabled = gpuNodes.length > 0; } @@ -93,7 +97,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) fileMangerConfig: global.AppConfig.launchpad.fileManger, CURRENCY: global.AppConfig.launchpad.currencySymbol || Coin.shellCoin, SEALOS_USER_DOMAINS: global.AppConfig.cloud.userDomains || [], - DESKTOP_DOMAIN: global.AppConfig.cloud.desktopDomain + DESKTOP_DOMAIN: global.AppConfig.cloud.desktopDomain, + PVC_STORAGE_MAX: global.AppConfig.launchpad.pvcStorageMax || 20, + GPU_ENABLED: global.AppConfig.common.gpuEnabled } }); } catch (error) { diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx index 7b708171e24..2a414493d23 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx @@ -575,7 +575,8 @@ const Form = ({ value={getValues('hpa.target')} list={[ { value: 'cpu', label: t('CPU') }, - { value: 'memory', label: t('Memory') } + { value: 'memory', label: t('Memory') }, + ...(userSourcePrice?.gpu ? [{ label: 'GPU', value: 'gpu' }] : []) ]} onchange={(val: any) => setValue('hpa.target', val)} /> diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx index 9a26406738f..98ac957f8c0 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx @@ -22,6 +22,7 @@ import MyFormControl from '@/components/FormControl'; import { useTranslation } from 'next-i18next'; import { pathToNameFormat } from '@/utils/tools'; import { MyTooltip } from '@sealos/ui'; +import { PVC_STORAGE_MAX } from '@/store/static'; export type StoreType = { id?: string; @@ -82,8 +83,8 @@ const StoreModal = ({ {t('capacity')} - - + + diff --git a/frontend/providers/applaunchpad/src/store/static.ts b/frontend/providers/applaunchpad/src/store/static.ts index 10c8bd2a79e..e03e47557d3 100644 --- a/frontend/providers/applaunchpad/src/store/static.ts +++ b/frontend/providers/applaunchpad/src/store/static.ts @@ -9,10 +9,13 @@ export let SHOW_EVENT_ANALYZE = false; export let CURRENCY = Coin.shellCoin; export let UPLOAD_LIMIT = 50; export let DOWNLOAD_LIMIT = 100; +export let PVC_STORAGE_MAX = 20; +export let GPU_ENABLED = false; export const loadInitData = async () => { try { const res = await getInitData(); + SEALOS_DOMAIN = res.SEALOS_DOMAIN; SEALOS_USER_DOMAINS = res.SEALOS_USER_DOMAINS; DOMAIN_PORT = res.DOMAIN_PORT; @@ -21,13 +24,16 @@ export const loadInitData = async () => { UPLOAD_LIMIT = res.fileMangerConfig.uploadLimit; DOWNLOAD_LIMIT = res.fileMangerConfig.downloadLimit; DESKTOP_DOMAIN = res.DESKTOP_DOMAIN; + PVC_STORAGE_MAX = res.PVC_STORAGE_MAX; + GPU_ENABLED = res.GPU_ENABLED; return { SEALOS_DOMAIN, DOMAIN_PORT, CURRENCY, FORM_SLIDER_LIST_CONFIG: res.FORM_SLIDER_LIST_CONFIG, - DESKTOP_DOMAIN: res.DESKTOP_DOMAIN + DESKTOP_DOMAIN: res.DESKTOP_DOMAIN, + GPU_ENABLED }; } catch (error) {} diff --git a/frontend/providers/applaunchpad/src/types/app.d.ts b/frontend/providers/applaunchpad/src/types/app.d.ts index e6539a03fb0..737a09ec384 100644 --- a/frontend/providers/applaunchpad/src/types/app.d.ts +++ b/frontend/providers/applaunchpad/src/types/app.d.ts @@ -13,7 +13,7 @@ import type { } from '@kubernetes/client-node'; import { MonitorDataResult } from './monitor'; -export type HpaTarget = 'cpu' | 'memory'; +export type HpaTarget = 'cpu' | 'memory' | 'gpu'; export type DeployKindsType = | V1Deployment diff --git a/frontend/providers/applaunchpad/src/types/index.d.ts b/frontend/providers/applaunchpad/src/types/index.d.ts index 2d3df3c88b5..51cbc29e43c 100644 --- a/frontend/providers/applaunchpad/src/types/index.d.ts +++ b/frontend/providers/applaunchpad/src/types/index.d.ts @@ -41,6 +41,7 @@ export type AppConfigType = { }; launchpad: { currencySymbol: Coin; + pvcStorageMax: number; eventAnalyze: { enabled: boolean; fastGPTKey?: string; diff --git a/frontend/providers/applaunchpad/src/utils/adapt.ts b/frontend/providers/applaunchpad/src/utils/adapt.ts index 5b5b723db81..fe7692462c9 100644 --- a/frontend/providers/applaunchpad/src/utils/adapt.ts +++ b/frontend/providers/applaunchpad/src/utils/adapt.ts @@ -335,13 +335,20 @@ export const adaptAppDetail = async (configs: DeployKindsType[]): Promise { + const metrics = deployKindsMap.HorizontalPodAutoscaler.spec.metrics?.[0]; + if (metrics?.pods?.metric?.name === 'DCGM_FI_DEV_GPU_UTIL') { + return Number(metrics.pods.target?.averageValue) || 50; + } + return metrics?.resource?.target?.averageUtilization + ? metrics.resource.target.averageUtilization / 10 + : 50; + })(), minReplicas: deployKindsMap.HorizontalPodAutoscaler.spec.minReplicas || 3, maxReplicas: deployKindsMap.HorizontalPodAutoscaler.spec.maxReplicas || 10 } diff --git a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts index a290b372ed8..a669acb0cff 100644 --- a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts +++ b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts @@ -447,38 +447,56 @@ export const json2HPA = (data: AppEditType) => { }, minReplicas: str2Num(data.hpa?.minReplicas), maxReplicas: str2Num(data.hpa?.maxReplicas), - metrics: [ - { - type: 'Resource', - resource: { - name: data.hpa.target, - target: { - type: 'Utilization', - averageUtilization: str2Num(data.hpa.value * 10) - } + metrics: + data.hpa.target === 'gpu' + ? [ + { + pods: { + metric: { + name: 'DCGM_FI_DEV_GPU_UTIL' + }, + target: { + averageValue: data.hpa.value.toString(), + type: 'AverageValue' + } + }, + type: 'Pods' + } + ] + : [ + { + type: 'Resource', + resource: { + name: data.hpa.target, + target: { + type: 'Utilization', + averageUtilization: str2Num(data.hpa.value * 10) + } + } + } + ], + ...(data.hpa.target !== 'gpu' && { + behavior: { + scaleDown: { + policies: [ + { + type: 'Pods', + value: 1, + periodSeconds: 60 + } + ] + }, + scaleUp: { + policies: [ + { + type: 'Pods', + value: 1, + periodSeconds: 60 + } + ] } } - ], - behavior: { - scaleDown: { - policies: [ - { - type: 'Pods', - value: 1, - periodSeconds: 60 - } - ] - }, - scaleUp: { - policies: [ - { - type: 'Pods', - value: 1, - periodSeconds: 60 - } - ] - } - } + }) } }; return yaml.dump(template); diff --git a/frontend/providers/costcenter/src/components/CurrencySymbol.tsx b/frontend/providers/costcenter/src/components/CurrencySymbol.tsx index ef16339b11c..bb50cba8657 100644 --- a/frontend/providers/costcenter/src/components/CurrencySymbol.tsx +++ b/frontend/providers/costcenter/src/components/CurrencySymbol.tsx @@ -8,7 +8,7 @@ export default function CurrencySymbol({ } & IconProps & TextProps) { return type === 'shellCoin' ? ( - + ) : type === 'cny' ? ( ) : ( diff --git a/frontend/providers/costcenter/src/components/RechargeModal.tsx b/frontend/providers/costcenter/src/components/RechargeModal.tsx index b39c30068a7..499715d0789 100644 --- a/frontend/providers/costcenter/src/components/RechargeModal.tsx +++ b/frontend/providers/costcenter/src/components/RechargeModal.tsx @@ -195,7 +195,7 @@ const BonusBox = (props: { - ) : ( + ) : props.bouns !== 0 ? ( {props.bouns} + ) : ( + <> )} @@ -345,6 +347,7 @@ const RechargeModal = forwardRef( >('/api/price/bonus'), {} ); + const [defaultSteps, ratios, steps, specialBonus] = useMemo(() => { const defaultSteps = Object.entries(bonuses?.data?.discount.defaultSteps || {}).sort( (a, b) => +a[0] - +b[0] @@ -377,7 +380,12 @@ const RechargeModal = forwardRef( const { stripeEnabled, wechatEnabled } = useEnvStore(); useEffect(() => { if (steps && steps.length > 0) { - setAmount(steps[0]); + const result = steps.map((v, idx) => [v, getBonus(v), idx]).filter(([k, v]) => v > 0); + if (result.length > 0) { + const [key, bouns, idx] = result[0]; + setSelectAmount(idx); + setAmount(key); + } } }, [steps]); const handleWechatConfirm = () => { @@ -674,6 +682,7 @@ const RechargeModal = forwardRef( .filter(([_, _2, ratio], idx) => { return ratio > 0; }) + .map(([pre, next, ratio], idx) => ( <> diff --git a/frontend/providers/cronjob/deploy/Kubefile b/frontend/providers/cronjob/deploy/Kubefile index d1537f2f426..3dca0d3f3f4 100644 --- a/frontend/providers/cronjob/deploy/Kubefile +++ b/frontend/providers/cronjob/deploy/Kubefile @@ -9,4 +9,7 @@ ENV cloudDomain="127.0.0.1.nip.io" ENV cloudPort="" ENV certSecretName="wildcard-cert" +ENV SUCCESSFUL_JOBS_HISTORY_LIMIT="3" +ENV FAILED_JOBS_HISTORY_LIMIT="3" + CMD ["kubectl apply -f manifests"] diff --git a/frontend/providers/cronjob/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/cronjob/deploy/manifests/deploy.yaml.tmpl index 2c00f4930d6..6b1cb420ece 100644 --- a/frontend/providers/cronjob/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/cronjob/deploy/manifests/deploy.yaml.tmpl @@ -41,6 +41,10 @@ spec: value: {{ .cloudDomain }} - name: APPLAUNCHPAD_URL value: applaunchpad.{{ .cloudDomain }} + - name: SUCCESSFUL_JOBS_HISTORY_LIMIT + value: "{{ .SUCCESSFUL_JOBS_HISTORY_LIMIT }}" + - name: FAILED_JOBS_HISTORY_LIMIT + value: "{{ .FAILED_JOBS_HISTORY_LIMIT }}" securityContext: runAsNonRoot: true runAsUser: 1001 diff --git a/frontend/providers/cronjob/src/pages/api/platform/getEnv.ts b/frontend/providers/cronjob/src/pages/api/platform/getEnv.ts index 7ca46baf7d5..2e8044e0795 100644 --- a/frontend/providers/cronjob/src/pages/api/platform/getEnv.ts +++ b/frontend/providers/cronjob/src/pages/api/platform/getEnv.ts @@ -6,13 +6,17 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export type EnvResponse = { domain: string; applaunchpadUrl: string; + successfulJobsHistoryLimit: number; + failedJobsHistoryLimit: number; }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { jsonRes(res, { data: { domain: process.env.SEALOS_DOMAIN || defaultDomain, - applaunchpadUrl: process.env.APPLAUNCHPAD_URL || `applaunchpad.${process.env.SEALOS_DOMAIN}` + applaunchpadUrl: process.env.APPLAUNCHPAD_URL || `applaunchpad.${process.env.SEALOS_DOMAIN}`, + successfulJobsHistoryLimit: Number(process.env.SUCCESSFUL_JOBS_HISTORY_LIMIT) || 3, + failedJobsHistoryLimit: Number(process.env.FAILED_JOBS_HISTORY_LIMIT) || 3 } }); } diff --git a/frontend/providers/cronjob/src/store/env.ts b/frontend/providers/cronjob/src/store/env.ts index 79197e489b1..82db7872dad 100644 --- a/frontend/providers/cronjob/src/store/env.ts +++ b/frontend/providers/cronjob/src/store/env.ts @@ -13,7 +13,9 @@ const useEnvStore = create()( immer((set, get) => ({ SystemEnv: { domain: '', - applaunchpadUrl: '' + applaunchpadUrl: '', + successfulJobsHistoryLimit: 3, + failedJobsHistoryLimit: 3 }, initSystemEnv: async () => { const data = await getPlatformEnv(); diff --git a/frontend/providers/cronjob/src/utils/json2Yaml.ts b/frontend/providers/cronjob/src/utils/json2Yaml.ts index 982ea8ce2de..191c8b4c400 100644 --- a/frontend/providers/cronjob/src/utils/json2Yaml.ts +++ b/frontend/providers/cronjob/src/utils/json2Yaml.ts @@ -8,7 +8,8 @@ import useEnvStore from '@/store/env'; export const json2CronJob = (data: CronJobEditType) => { const timeZone = getUserTimeZone(); const kcHeader = encodeURIComponent(getUserKubeConfig()); - const { applaunchpadUrl } = useEnvStore.getState().SystemEnv; + const { applaunchpadUrl, successfulJobsHistoryLimit, failedJobsHistoryLimit } = + useEnvStore.getState().SystemEnv; const metadata = { name: data.jobName, @@ -111,8 +112,8 @@ export const json2CronJob = (data: CronJobEditType) => { schedule: data.schedule, concurrencyPolicy: 'Replace', startingDeadlineSeconds: 60, - successfulJobsHistoryLimit: 3, - failedJobsHistoryLimit: 3, + successfulJobsHistoryLimit, + failedJobsHistoryLimit, timeZone: timeZone, jobTemplate: { activeDeadlineSeconds: 600, diff --git a/frontend/providers/dbprovider/.vscode/settings.json b/frontend/providers/dbprovider/.vscode/settings.json index be18ba21857..bc175c84cd6 100644 --- a/frontend/providers/dbprovider/.vscode/settings.json +++ b/frontend/providers/dbprovider/.vscode/settings.json @@ -17,7 +17,6 @@ "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", "i18n-ally.extract.targetPickingStrategy": "most-similar-by-key", "i18n-ally.translate.engines": [ - "deepl", "google", ] } \ No newline at end of file diff --git a/frontend/providers/dbprovider/public/locales/en/common.json b/frontend/providers/dbprovider/public/locales/en/common.json index 93c6f82c577..373942dc3d8 100644 --- a/frontend/providers/dbprovider/public/locales/en/common.json +++ b/frontend/providers/dbprovider/public/locales/en/common.json @@ -40,7 +40,7 @@ "Running": "Running", "Saturday": "Sat", "Save": "Save", - "SaveTime": "Retention Period", + "SaveTime": "Retention", "Start": "Start", "Starting": "Starting", "Success": "succeeded", @@ -80,6 +80,7 @@ "backup_name_cannot_empty": "Must provide backup name", "backup_processing": "Saving Backup", "backup_running": "Saving Backup", + "backup_settings": "Backup", "backup_success_tip": "The backup task has been successfully created", "backup_time": "Backup Timestamp", "balance": "balance", @@ -117,7 +118,7 @@ "copy_success": "Copy succeeded", "covering_risks": "Coverage Risks", "cpu": "CPU", - "create_db": "Create New Database", + "create_db": "Create Database", "creation_time": "Creation Time", "current_connections": "Current Connections", "data_migration_config": "Data Migration Settings", @@ -129,6 +130,7 @@ "database_name": "Database Name", "database_name_cannot_empty": "Database name cannot be empty", "database_name_empty": "Application name is required", + "database_name_max_length": "Database name length cannot exceed {{length}} characters", "database_name_regex": "Start with a letter, only allow lowercase letters, numbers and -", "database_name_regex_error": "Database name can only contain lowercase letters, numbers, -, and must start with a letter.", "database_password_empty": "Please enter the database password", @@ -201,6 +203,7 @@ "guide_terminal_button": "Convenient terminal connection method improves data processing efficiency", "have_error": "Failed", "hits_ratio": "Hits Ratio", + "hour": "hour", "import_through_file": "Import via File", "important_tips_for_migrating": "Tip: Create a new database in sink DB if source_database and sink_database have overlapping data, to avoid conflicts", "innodb_buffer_pool": "InnoDB Buffer Pool", @@ -289,6 +292,7 @@ "start_hour": "Hour", "start_minute": "Minute", "start_success": "Database started. Please wait...", + "start_time": "Start Time", "status": "Status", "storage": "Storage", "storage_cannot_empty": "Please specify storage size", @@ -300,6 +304,8 @@ "successfully_closed_external_network_access": "Internet access disabled", "table_locks": "Table-level locks", "terminated_logs": "Terminated logs", + "termination_policy": "Delete Policy", + "termination_policy_tip": "Whether to keep a backup when deleting the database", "total_price": "total_price", "total_price_tip": "The estimated cost does not include port fees and traffic fees, and is subject to actual usage.", "turn_on": "Enable", @@ -317,5 +323,8 @@ "within_5_minutes": "Within 5 minutes", "yaml_file": "YAML", "you_have_successfully_deployed_database": "You have successfully deployed and created a database!", - "database_name_max_length": "Database name length cannot exceed {{length}} characters" + "delete_backup_with_db": "Keep Backups", + "delete_backup_with_db_tip": "Delete the databases but leave the backups as they are", + "wipeout_backup_with_db": "Discard Backups", + "wipeout_backup_with_db_tip": "Delete the databases and the backups" } \ No newline at end of file diff --git a/frontend/providers/dbprovider/public/locales/zh/common.json b/frontend/providers/dbprovider/public/locales/zh/common.json index 8c5fe2582b8..7b6056a88cc 100644 --- a/frontend/providers/dbprovider/public/locales/zh/common.json +++ b/frontend/providers/dbprovider/public/locales/zh/common.json @@ -80,6 +80,7 @@ "backup_name_cannot_empty": "备份名称不能为空", "backup_processing": "备份中", "backup_running": "备份中", + "backup_settings": "备份设置", "backup_success_tip": "备份任务已经成功创建", "backup_time": "备份时间", "balance": "余额", @@ -129,6 +130,7 @@ "database_name": "新数据库名", "database_name_cannot_empty": "数据库名称不能为空", "database_name_empty": "应用名称不能为空", + "database_name_max_length": "数据库名长度不能超过 {{length}} 个字符", "database_name_regex": "字母开头,仅能包含小写字母、数字和 -", "database_name_regex_error": "数据库名只能包含小写字母、数字和 -,并且字母开头。", "database_password_empty": "缺少数据库密码", @@ -201,6 +203,7 @@ "guide_terminal_button": "便捷的终端连接方式,提升数据处理效率", "have_error": "出现异常", "hits_ratio": "命中率", + "hour": "时", "import_through_file": "文件导入", "important_tips_for_migrating": "如果 source 数据库中 source_database 和 sink 数据库中 sink_database 的数据库有重叠,应该在sink 数据库中新建 database,以免出现数据重叠", "innodb_buffer_pool": "InnoDB 缓冲池", @@ -246,6 +249,7 @@ "migration_successful": "迁移成功", "migration_task_created_successfully": "迁移任务创建成功", "min_replicas": "实例数最小为: ", + "minute": "分", "monitor_list": "实时监控", "multi_replica_redis_tip": "Redis 多副本包含 HA 节点,请悉知,预估价格已包含 HA 节点费用", "name": "名字", @@ -289,6 +293,7 @@ "start_hour": "小时", "start_minute": "分钟", "start_success": "数据库启动成功,请等待", + "start_time": "开始时间", "status": "状态", "storage": "磁盘", "storage_cannot_empty": "容量不能为空", @@ -300,6 +305,8 @@ "successfully_closed_external_network_access": "已关闭外网访问", "table_locks": "表锁", "terminated_logs": "中断前", + "termination_policy": "删除策略", + "termination_policy_tip": "删除数据库时是否保留备份", "total_price": "总价", "total_price_tip": "预估费用不包括端口费用和流量费用,以实际使用为准", "turn_on": "开启", @@ -317,5 +324,8 @@ "within_5_minutes": "五分钟内", "yaml_file": "YAML 文件", "you_have_successfully_deployed_database": "您已成功部署创建一个数据库!", - "database_name_max_length": "数据库名长度不能超过 {{length}} 个字符" + "delete_backup_with_db": "保留备份", + "delete_backup_with_db_tip": "在删除数据库时,保留其备份", + "wipeout_backup_with_db": "随数据库删除", + "wipeout_backup_with_db_tip": "在删除数据库时,删除其备份" } \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/api/backup.ts b/frontend/providers/dbprovider/src/api/backup.ts index 02c0baa180e..70b8f14d151 100644 --- a/frontend/providers/dbprovider/src/api/backup.ts +++ b/frontend/providers/dbprovider/src/api/backup.ts @@ -4,20 +4,6 @@ import { adaptBackup, adaptBackupByCluster, adaptDBDetail } from '@/utils/adapt' import { AutoBackupFormType } from '@/types/backup'; import type { Props as UpdatePolicyProps } from '@/pages/api/backup/updatePolicy'; -/** - * Deprecated API: This endpoint is no longer supported. - * - * The new method for obtaining backup policies is by querying the 'cluster spec backup' endpoint - * for the specific database in the cluster. - * - * To update the auto-backup policy, use the PATCH operation on the 'cluster spec backup' resource. - * @deprecated - * @param data - Object containing information about the database, including dbName and dbType. - * @returns {Promise} - A promise resolving to the auto-backup configuration form. - */ -export const getBackupPolicy = (data: { dbName: string; dbType: string }) => - GET(`/api/backup/policy`, data); - export const createBackup = (data: CreateBackupPros) => POST('/api/backup/create', data); export const getBackupList = (dbName: string) => diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/backupSettings.svg b/frontend/providers/dbprovider/src/components/Icon/icons/backupSettings.svg new file mode 100644 index 00000000000..1c399a73cc4 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/backupSettings.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/index.tsx b/frontend/providers/dbprovider/src/components/Icon/index.tsx index 3e03b06897d..b4b35735a65 100644 --- a/frontend/providers/dbprovider/src/components/Icon/index.tsx +++ b/frontend/providers/dbprovider/src/components/Icon/index.tsx @@ -53,6 +53,7 @@ const map = { import: require('./icons/import.svg').default, file: require('./icons/file.svg').default, config: require('./icons/config.svg').default, + backupSettings: require('./icons/backupSettings.svg').default, monitor: require('./icons/monitor.svg').default, arrowDown: require('./icons/arrowDown.svg').default, docs: require('./icons/docs.svg').default diff --git a/frontend/providers/dbprovider/src/constants/db.ts b/frontend/providers/dbprovider/src/constants/db.ts index 2806376806b..aa1a527c74b 100644 --- a/frontend/providers/dbprovider/src/constants/db.ts +++ b/frontend/providers/dbprovider/src/constants/db.ts @@ -7,6 +7,7 @@ import { DBSourceType } from '@/types/db'; import { CpuSlideMarkList, MemorySlideMarkList } from './editApp'; +import { I18nCommonKey } from '@/types/i18next'; export const crLabelKey = 'sealos-db-provider-cr'; export const CloudMigraionLabel = 'sealos-db-provider-cr-migrate'; @@ -270,7 +271,17 @@ export const defaultDBEditValue: DBEditType = { cpu: CpuSlideMarkList[1].value, memory: MemorySlideMarkList[1].value, storage: 3, - labels: {} + labels: {}, + autoBackup: { + start: true, + type: 'day', + week: [], + hour: '23', + minute: '00', + saveTime: 7, + saveType: 'd' + }, + terminationPolicy: 'Delete' }; export const defaultDBDetail: DBDetailType = { @@ -428,3 +439,28 @@ export const DBSourceConfigs: Array<{ { key: templateDeployKey, type: 'app_store' }, { key: sealafDeployKey, type: 'sealaf' } ]; + +export const SelectTimeList = new Array(60).fill(0).map((item, i) => { + const val = i < 10 ? `0${i}` : `${i}`; + return { + id: val, + label: val + }; +}); + +export const WeekSelectList: { label: I18nCommonKey; id: string }[] = [ + { label: 'Monday', id: '1' }, + { label: 'Tuesday', id: '2' }, + { label: 'Wednesday', id: '3' }, + { label: 'Thursday', id: '4' }, + { label: 'Friday', id: '5' }, + { label: 'Saturday', id: '6' }, + { label: 'Sunday', id: '0' } +]; + +export const BackupSupportedDBTypeList: DBType[] = [ + 'postgresql', + 'mongodb', + 'apecloud-mysql', + 'redis' +]; diff --git a/frontend/providers/dbprovider/src/constants/theme.ts b/frontend/providers/dbprovider/src/constants/theme.ts index 71bb2020bcf..b8638768891 100644 --- a/frontend/providers/dbprovider/src/constants/theme.ts +++ b/frontend/providers/dbprovider/src/constants/theme.ts @@ -1,5 +1,35 @@ import { extendTheme } from '@chakra-ui/react'; import { theme as SealosTheme } from '@sealos/ui'; +import { checkboxAnatomy } from '@chakra-ui/anatomy'; +import { createMultiStyleConfigHelpers } from '@chakra-ui/react'; + +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers( + checkboxAnatomy.keys +); + +const checkbox = defineMultiStyleConfig({ + baseStyle: { + control: { + borderWidth: '1px', + borderRadius: '4px', + _checked: { + bg: '#F0FBFF', + borderColor: '#219BF4', + boxShadow: ' 0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)', + _hover: { + bg: '#F0FBFF', + borderColor: '#219BF4' + } + }, + _hover: { + bg: 'transparent' + } + }, + icon: { + color: '#219BF4' + } + } +}); export const theme = extendTheme(SealosTheme, { styles: { @@ -13,5 +43,6 @@ export const theme = extendTheme(SealosTheme, { minWidth: '700px' } } - } + }, + components: { Checkbox: checkbox } }); diff --git a/frontend/providers/dbprovider/src/pages/api/backup/policy.ts b/frontend/providers/dbprovider/src/pages/api/backup/policy.ts deleted file mode 100644 index e87b1b4c5ff..00000000000 --- a/frontend/providers/dbprovider/src/pages/api/backup/policy.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import { ApiResp } from '@/services/kubernet'; -import { authSession } from '@/services/backend/auth'; -import { getK8s } from '@/services/backend/kubernetes'; -import { jsonRes } from '@/services/backend/response'; -import { adaptPolicy } from '@/utils/adapt'; -import { DBBackupPolicyNameMap, DBTypeEnum } from '@/constants/db'; - -export type Props = { - dbName: string; - dbType: `${DBTypeEnum}`; -}; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { dbName, dbType } = req.query as Props; - - if (!dbName || !dbType) { - jsonRes(res, { - code: 500, - error: 'params error' - }); - return; - } - - const group = 'dataprotection.kubeblocks.io'; - const version = 'v1alpha1'; - const plural = 'backuppolicies'; - - try { - const { k8sCustomObjects, namespace } = await getK8s({ - kubeconfig: await authSession(req) - }); - - // get backup backupolicies.dataprotection.kubeblocks.io - const { body } = (await k8sCustomObjects.getNamespacedCustomObject( - group, - version, - namespace, - plural, - `${dbName}-${DBBackupPolicyNameMap[dbType]}-backup-policy` - )) as { body: any }; - - jsonRes(res, { - data: adaptPolicy(body) - }); - } catch (err: any) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts b/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts index 739845706af..33087a3da7f 100644 --- a/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts +++ b/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts @@ -1,10 +1,11 @@ import { BACKUP_REPO_DEFAULT_KEY } from '@/constants/backup'; import { DBTypeEnum } from '@/constants/db'; import { authSession } from '@/services/backend/auth'; -import { K8sApi, K8sApiDefault, getK8s } from '@/services/backend/kubernetes'; +import { K8sApiDefault, getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; import { BackupRepoCRItemType } from '@/types/backup'; +import { KbPgClusterType } from '@/types/cluster'; import * as k8s from '@kubernetes/client-node'; import { PatchUtils } from '@kubernetes/client-node'; import type { NextApiRequest, NextApiResponse } from 'next'; @@ -12,13 +13,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export type Props = { dbName: string; dbType: `${DBTypeEnum}`; - autoBackup?: { - enabled: boolean; - cronExpression: string; - method: string; - retentionPeriod: string; - repoName: string; - }; + autoBackup?: KbPgClusterType['spec']['backup']; }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -31,66 +26,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } - const group = 'apps.kubeblocks.io'; - const version = 'v1alpha1'; - const plural = 'clusters'; - try { const { k8sCustomObjects, namespace } = await getK8s({ kubeconfig: await authSession(req) }); - // Get cluster backup repository - const kc = K8sApiDefault(); - const backupRepos = (await kc - .makeApiClient(k8s.CustomObjectsApi) - .listClusterCustomObject('dataprotection.kubeblocks.io', 'v1alpha1', 'backuprepos')) as { - body: { - items: BackupRepoCRItemType[]; - }; - }; - - const defaultRepoItem = backupRepos?.body?.items?.find( - (item) => item.metadata.annotations[BACKUP_REPO_DEFAULT_KEY] === 'true' - ); - - const backupRepoName = defaultRepoItem?.metadata?.name; - - if (!backupRepoName) { - throw new Error('Missing backup repository'); - } - const patch = autoBackup - ? [ - { - op: 'replace', - path: '/spec/backup', - value: { - ...autoBackup, - repoName: backupRepoName - } - } - ] - : [ - { - op: 'replace', - path: '/spec/backup/enabled', - value: false - } - ]; - - // get backup backupolicies.dataprotection.kubeblocks.io - const result = await k8sCustomObjects.patchNamespacedCustomObject( - group, - version, - namespace, - plural, + const result = await updateBackupPolicyApi({ dbName, - patch, - undefined, - undefined, - undefined, - { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_PATCH } } - ); + dbType, + autoBackup, + k8sCustomObjects, + namespace + }); jsonRes(res, { data: result?.body }); } catch (err: any) { @@ -100,3 +47,79 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } } + +export type UpdateBackupPolicyParams = { + dbName: string; + dbType: `${DBTypeEnum}`; + autoBackup?: KbPgClusterType['spec']['backup']; + k8sCustomObjects: k8s.CustomObjectsApi; + namespace: string; +}; + +export async function updateBackupPolicyApi({ + dbName, + dbType, + autoBackup, + k8sCustomObjects, + namespace +}: UpdateBackupPolicyParams) { + const group = 'apps.kubeblocks.io'; + const version = 'v1alpha1'; + const plural = 'clusters'; + + // Get cluster backup repository + const kc = K8sApiDefault(); + const backupRepos = (await kc + .makeApiClient(k8s.CustomObjectsApi) + .listClusterCustomObject('dataprotection.kubeblocks.io', 'v1alpha1', 'backuprepos')) as { + body: { + items: BackupRepoCRItemType[]; + }; + }; + + const defaultRepoItem = backupRepos?.body?.items?.find( + (item) => item.metadata.annotations[BACKUP_REPO_DEFAULT_KEY] === 'true' + ); + + const backupRepoName = defaultRepoItem?.metadata?.name; + + if (!backupRepoName) { + throw new Error('Missing backup repository'); + } + + const patch = autoBackup + ? [ + { + op: 'replace', + path: '/spec/backup', + value: { + ...autoBackup, + repoName: backupRepoName + } + } + ] + : [ + { + op: 'replace', + path: '/spec/backup/enabled', + value: false + } + ]; + + console.log('backup patch', patch); + + const result = await k8sCustomObjects.patchNamespacedCustomObject( + group, + version, + namespace, + plural, + dbName, + patch, + undefined, + undefined, + undefined, + { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_PATCH } } + ); + + return result; +} diff --git a/frontend/providers/dbprovider/src/pages/api/createDB.ts b/frontend/providers/dbprovider/src/pages/api/createDB.ts index 76126e612c9..7b94e2832ac 100644 --- a/frontend/providers/dbprovider/src/pages/api/createDB.ts +++ b/frontend/providers/dbprovider/src/pages/api/createDB.ts @@ -6,6 +6,10 @@ import { KbPgClusterType } from '@/types/cluster'; import { BackupItemType, DBEditType } from '@/types/db'; import { json2Account, json2ClusterOps, json2CreateCluster } from '@/utils/json2Yaml'; import type { NextApiRequest, NextApiResponse } from 'next'; +import { updateBackupPolicyApi } from './backup/updatePolicy'; +import { BackupSupportedDBTypeList } from '@/constants/db'; +import { convertBackupFormToSpec } from '@/utils/adapt'; +import { CustomObjectsApi, PatchUtils } from '@kubernetes/client-node'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -39,7 +43,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< 'Gi', '' ) - ) + ), + terminationPolicy: body.spec.terminationPolicy }; const opsRequests = []; @@ -77,6 +82,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } + if (BackupSupportedDBTypeList.includes(dbForm.dbType) && dbForm?.autoBackup) { + const autoBackup = convertBackupFormToSpec({ + autoBackup: dbForm?.autoBackup, + dbType: dbForm.dbType + }); + + await updateBackupPolicyApi({ + dbName: dbForm.dbName, + dbType: dbForm.dbType, + autoBackup, + k8sCustomObjects, + namespace + }); + + if (currentConfig.terminationPolicy !== dbForm.terminationPolicy) { + await updateTerminationPolicyApi({ + dbName: dbForm.dbName, + terminationPolicy: dbForm.terminationPolicy, + k8sCustomObjects, + namespace + }); + } + } + return jsonRes(res, { data: 'success update db' }); @@ -101,6 +130,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await applyYamlList([updateAccountYaml], 'replace'); + if (BackupSupportedDBTypeList.includes(dbForm.dbType) && dbForm?.autoBackup) { + const autoBackup = convertBackupFormToSpec({ + autoBackup: dbForm?.autoBackup, + dbType: dbForm.dbType + }); + + await updateBackupPolicyApi({ + dbName: dbForm.dbName, + dbType: dbForm.dbType, + autoBackup, + k8sCustomObjects, + namespace + }); + } + jsonRes(res, { data: 'success create db' }); @@ -111,3 +155,42 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } } + +export async function updateTerminationPolicyApi({ + dbName, + terminationPolicy, + k8sCustomObjects, + namespace +}: { + dbName: string; + terminationPolicy: string; + k8sCustomObjects: CustomObjectsApi; + namespace: string; +}) { + const group = 'apps.kubeblocks.io'; + const version = 'v1alpha1'; + const plural = 'clusters'; + + const patch = [ + { + op: 'replace', + path: '/spec/terminationPolicy', + value: terminationPolicy + } + ]; + + const result = await k8sCustomObjects.patchNamespacedCustomObject( + group, + version, + namespace, + plural, + dbName, + patch, + undefined, + undefined, + undefined, + { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_PATCH } } + ); + + return result; +} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx index c4275efaab1..1174a2df80a 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx @@ -1,6 +1,6 @@ import { createBackup, updateBackupPolicy } from '@/api/backup'; import Tip from '@/components/Tip'; -import { DBBackupMethodNameMap, DBTypeEnum } from '@/constants/db'; +import { DBBackupMethodNameMap, DBTypeEnum, SelectTimeList, WeekSelectList } from '@/constants/db'; import { useConfirm } from '@/hooks/useConfirm'; import type { AutoBackupFormType, AutoBackupType } from '@/types/backup'; import { I18nCommonKey } from '@/types/i18next'; @@ -83,31 +83,6 @@ const BackupModal = ({ defaultValues: defaultVal }); - const selectTimeList = useRef< - { - id: string; - label: string; - }[] - >( - (() => - new Array(60).fill(0).map((item, i) => { - const val = i < 10 ? `0${i}` : `${i}`; - return { - id: val, - label: val - }; - }))() - ); - const weekSelectList: MutableRefObject<{ label: I18nCommonKey; id: string }[]> = useRef([ - { label: 'Monday', id: '1' }, - { label: 'Tuesday', id: '2' }, - { label: 'Wednesday', id: '3' }, - { label: 'Thursday', id: '4' }, - { label: 'Friday', id: '5' }, - { label: 'Saturday', id: '6' }, - { label: 'Sunday', id: '0' } - ]); - const navStyle = useCallback( (nav: `${NavEnum}`) => ({ p: '8px', @@ -331,7 +306,7 @@ const BackupModal = ({ {getAutoValues('type') === 'week' && ( - {weekSelectList.current.map((item) => ( + {WeekSelectList.map((item) => ( ({ value: i.id, label: i.label }))} + list={SelectTimeList.slice(0, 24).map((i) => ({ + value: i.id, + label: i.label + }))} // icon={} onchange={(val: any) => { setAutoValue('hour', val); @@ -376,7 +352,7 @@ const BackupModal = ({ ({ + list={SelectTimeList.map((i) => ({ value: i.id, label: i.label }))} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/index.tsx index ff545e2f854..c882761518c 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/index.tsx @@ -22,6 +22,7 @@ import ReconfigureTable from './components/Reconfigure/index'; import useDetailDriver from '@/hooks/useDetailDriver'; import ErrorLog from '@/pages/db/detail/components/ErrorLog'; import MyIcon from '@/components/Icon'; +import { BackupSupportedDBTypeList } from '@/constants/db'; enum TabEnum { pod = 'pod', @@ -52,9 +53,7 @@ const AppDetail = ({ const { listNav } = useMemo(() => { const PublicNetMigration = ['postgresql', 'apecloud-mysql', 'mongodb'].includes(dbType); const MigrateSupported = ['postgresql', 'mongodb', 'apecloud-mysql'].includes(dbType); - const BackupSupported = - ['postgresql', 'mongodb', 'apecloud-mysql', 'redis'].includes(dbType) && - SystemEnv.BACKUP_ENABLED; + const BackupSupported = BackupSupportedDBTypeList.includes(dbType) && SystemEnv.BACKUP_ENABLED; const listNavValue = [ { diff --git a/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx b/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx index 83baeeb9cd9..b2bc5d6e8ff 100644 --- a/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx +++ b/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx @@ -3,11 +3,19 @@ import MyIcon from '@/components/Icon'; import PriceBox from '@/components/PriceBox'; import QuotaBox from '@/components/QuotaBox'; import Tip from '@/components/Tip'; -import { DBTypeEnum, DBTypeList, RedisHAConfig } from '@/constants/db'; +import { + BackupSupportedDBTypeList, + DBTypeEnum, + DBTypeList, + RedisHAConfig, + SelectTimeList, + WeekSelectList +} from '@/constants/db'; import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/editApp'; import useEnvStore from '@/store/env'; import { DBVersionMap, INSTALL_ACCOUNT } from '@/store/static'; import type { QueryType } from '@/types'; +import { AutoBackupType } from '@/types/backup'; import type { DBEditType } from '@/types/db'; import { I18nCommonKey } from '@/types/i18next'; import { InfoOutlineIcon } from '@chakra-ui/icons'; @@ -15,6 +23,8 @@ import { Box, Button, Center, + Checkbox, + Collapse, Flex, FormControl, Grid, @@ -25,6 +35,7 @@ import { NumberInput, NumberInputField, NumberInputStepper, + Switch, Text, useDisclosure, useTheme @@ -33,7 +44,7 @@ import { MySelect, MySlider, MyTooltip, RangeInput, Tabs } from '@sealos/ui'; import { throttle } from 'lodash'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; -import { useEffect, useMemo, useState } from 'react'; +import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'; import { UseFormReturn } from 'react-hook-form'; const Form = ({ @@ -64,6 +75,11 @@ const Form = ({ id: 'baseInfo', label: 'basic', icon: 'formInfo' + }, + { + id: 'backupSettings', + label: 'backup_settings', + icon: 'backupSettings' } ]; @@ -194,7 +210,7 @@ const Form = ({ name={item.icon as any} w={'20px'} h={'20px'} - color={activeNav === item.id ? 'grayModern.600' : 'myGray.400'} + color={activeNav === item.id ? 'grayModern.900' : 'grayModern.500'} /> {t(item.label)} @@ -504,6 +520,203 @@ const Form = ({ + {BackupSupportedDBTypeList.includes(getValues('dbType')) && ( + + + + {t('backup_settings')} + { + setValue('autoBackup.start', e.target.checked); + }} + /> + + + + + {t('CronExpression')} + { + setValue('autoBackup.type', e as AutoBackupType); + }} + /> + + {getValues('autoBackup.type') === 'week' && ( + + + {WeekSelectList.map((item) => ( + + { + const val = e.target.checked; + const checkedList = [...getValues('autoBackup.week')]; + const index = checkedList.findIndex((week) => week === item.id); + if (val && index === -1) { + setValue('autoBackup.week', checkedList.concat(item.id)); + } else if (!val && index > -1) { + checkedList.splice(index, 1); + setValue('autoBackup.week', checkedList); + } + }} + > + {t(item.label)} + + + ))} + + )} + + {t('start_time')} + {getValues('autoBackup.type') !== 'hour' && ( + + ({ + value: i.id, + label: i.label + }))} + onchange={(val: any) => { + setValue('autoBackup.hour', val); + }} + /> + + {t('hour')} + + + )} + + + ({ + value: i.id, + label: i.label + }))} + onchange={(val: any) => { + setValue('autoBackup.minute', val); + }} + /> + + {t('minute')} + + + + + + {t('SaveTime')} + + { + setValue('autoBackup.saveType', val); + }} + /> + + + {t('termination_policy')} + {/* { + setValue('terminationPolicy', e.target.checked ? 'Delete' : 'WipeOut'); + }} + /> */} + + {['Delete', 'WipeOut'].map((item) => { + const isChecked = getValues('terminationPolicy') === item; + + return ( + { + setValue( + 'terminationPolicy', + getValues('terminationPolicy') === 'Delete' ? 'WipeOut' : 'Delete' + ); + }} + cursor={'pointer'} + > +
+ {isChecked && ( + + )} +
+ + + {t(`${item.toLowerCase()}_backup_with_db` as I18nCommonKey)} + + + {t(`${item.toLowerCase()}_backup_with_db_tip` as I18nCommonKey)} + + +
+ ); + })} +
+
+
+
+
+ )} diff --git a/frontend/providers/dbprovider/src/pages/db/edit/index.tsx b/frontend/providers/dbprovider/src/pages/db/edit/index.tsx index f8d72d7324e..aa9e182131f 100644 --- a/frontend/providers/dbprovider/src/pages/db/edit/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/edit/index.tsx @@ -1,5 +1,5 @@ import { adapterMongoHaConfig, applyYamlList, createDB } from '@/api/db'; -import { defaultDBEditValue } from '@/constants/db'; +import { BackupSupportedDBTypeList, defaultDBEditValue } from '@/constants/db'; import { editModeMap } from '@/constants/editApp'; import { useConfirm } from '@/hooks/useConfirm'; import { useLoading } from '@/hooks/useLoading'; @@ -9,7 +9,7 @@ import { DBVersionMap } from '@/store/static'; import { useUserStore } from '@/store/user'; import type { YamlItemType } from '@/types'; import type { DBEditType } from '@/types/db'; -import { adaptDBForm } from '@/utils/adapt'; +import { adaptDBForm, convertBackupFormToSpec } from '@/utils/adapt'; import { serviceSideProps } from '@/utils/i18n'; import { json2Account, json2CreateCluster, limitRangeYaml } from '@/utils/json2Yaml'; import { Box, Flex } from '@chakra-ui/react'; @@ -25,6 +25,7 @@ import Form from './components/Form'; import Header from './components/Header'; import Yaml from './components/Yaml'; import useDriver from '@/hooks/useDriver'; +import { updateBackupPolicy } from '@/api/backup'; const ErrorModal = dynamic(() => import('@/components/ErrorModal')); diff --git a/frontend/providers/dbprovider/src/pages/db/migrate/index.tsx b/frontend/providers/dbprovider/src/pages/db/migrate/index.tsx index 62381c5d251..216f1ba3f65 100644 --- a/frontend/providers/dbprovider/src/pages/db/migrate/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/migrate/index.tsx @@ -39,7 +39,8 @@ const defaultEdit: MigrateForm = { sourceDatabase: '', sourceDatabaseTable: ['All'], isChecked: false, - continued: false + continued: false, + terminationPolicy: 'Delete' }; const EditApp = ({ diff --git a/frontend/providers/dbprovider/src/types/backup.d.ts b/frontend/providers/dbprovider/src/types/backup.d.ts index 7468cf44e40..a0b03bdf044 100644 --- a/frontend/providers/dbprovider/src/types/backup.d.ts +++ b/frontend/providers/dbprovider/src/types/backup.d.ts @@ -59,8 +59,8 @@ export interface BackupCRItemType { export type AutoBackupType = 'day' | 'hour' | 'week'; export type AutoBackupFormType = { - start: boolean; - type: AutoType; + start: boolean; // enable auto backup + type: AutoBackupType; week: string[]; hour: string; minute: string; diff --git a/frontend/providers/dbprovider/src/types/cluster.d.ts b/frontend/providers/dbprovider/src/types/cluster.d.ts index c75d11ec549..3ca22bead0d 100644 --- a/frontend/providers/dbprovider/src/types/cluster.d.ts +++ b/frontend/providers/dbprovider/src/types/cluster.d.ts @@ -20,10 +20,18 @@ export type KbPgClusterType = { status?: KubeBlockClusterStatus; }; +/** + * DoNotTerminate: The database deletion cannot be performed. + * Halt: deletes only database resources, but retains database disks. + * Delete: deletes only the database resource, but keeps the database backup. + * WipeOut: deletes all contents of the database. + */ +export type KubeBlockClusterTerminationPolicy = 'Delete' | 'WipeOut'; + export interface KubeBlockClusterSpec { clusterDefinitionRef: `${DBTypeEnum}`; clusterVersionRef: string; - terminationPolicy: string; + terminationPolicy: KubeBlockClusterTerminationPolicy; componentSpecs: { componentDefRef: `${DBTypeEnum}`; name: `${DBTypeEnum}`; @@ -54,7 +62,7 @@ export interface KubeBlockClusterSpec { enabled: boolean; cronExpression: string; method: string; - pitrEnabled: boolean; + pitrEnabled?: boolean; repoName: string; retentionPeriod: string; }; @@ -89,6 +97,9 @@ export type KbPodType = { }; }; +/** + * @deprecated + */ export type KubeBlockBackupPolicyType = { metadata: { name: string; diff --git a/frontend/providers/dbprovider/src/types/db.d.ts b/frontend/providers/dbprovider/src/types/db.d.ts index 054612e8636..48fe96769ff 100644 --- a/frontend/providers/dbprovider/src/types/db.d.ts +++ b/frontend/providers/dbprovider/src/types/db.d.ts @@ -13,6 +13,8 @@ import type { V1ContainerStatus } from '@kubernetes/client-node'; import { I18nCommonKey } from './i18next'; +import { AutoBackupFormType } from './backup'; +import { KubeBlockClusterTerminationPolicy } from './cluster'; export type DBType = `${DBTypeEnum}`; @@ -68,6 +70,8 @@ export interface DBEditType { memory: number; storage: number; labels: { [key: string]: string }; + terminationPolicy: KubeBlockClusterTerminationPolicy; + autoBackup?: AutoBackupFormType; } export type DBSourceType = 'app_store' | 'sealaf'; diff --git a/frontend/providers/dbprovider/src/types/migrate.d.ts b/frontend/providers/dbprovider/src/types/migrate.d.ts index 8a1fab267c0..6e343407868 100644 --- a/frontend/providers/dbprovider/src/types/migrate.d.ts +++ b/frontend/providers/dbprovider/src/types/migrate.d.ts @@ -1,3 +1,4 @@ +import { KubeBlockClusterTerminationPolicy } from './cluster'; import { DBType } from './db'; export enum InternetMigrationTemplate { @@ -113,6 +114,7 @@ export type MigrateForm = { isChecked: boolean; continued: boolean; remark?: string; + terminationPolicy: KubeBlockClusterTerminationPolicy; }; export interface MigrateItemType { diff --git a/frontend/providers/dbprovider/src/utils/adapt.ts b/frontend/providers/dbprovider/src/utils/adapt.ts index d8429ec9b10..d43342f0d3a 100644 --- a/frontend/providers/dbprovider/src/utils/adapt.ts +++ b/frontend/providers/dbprovider/src/utils/adapt.ts @@ -1,17 +1,14 @@ import { BACKUP_REMARK_LABEL_KEY, BackupTypeEnum, backupStatusMap } from '@/constants/backup'; import { + DBBackupMethodNameMap, DBPreviousConfigKey, DBReconfigStatusMap, DBSourceConfigs, MigrationRemark, dbStatusMap } from '@/constants/db'; -import type { AutoBackupFormType, BackupCRItemType } from '@/types/backup'; -import type { - KbPgClusterType, - KubeBlockBackupPolicyType, - KubeBlockOpsRequestType -} from '@/types/cluster'; +import type { AutoBackupFormType, AutoBackupType, BackupCRItemType } from '@/types/backup'; +import type { KbPgClusterType, KubeBlockOpsRequestType } from '@/types/cluster'; import type { DBDetailType, DBEditType, @@ -106,41 +103,57 @@ export const adaptDBDetail = (db: KbPgClusterType): DBDetailType => { conditions: db?.status?.conditions || [], isDiskSpaceOverflow: false, labels: db.metadata.labels || {}, - source: getDBSource(db) + source: getDBSource(db), + autoBackup: adaptBackupByCluster(db), + terminationPolicy: db.spec?.terminationPolicy || 'Delete' }; }; export const adaptBackupByCluster = (db: KbPgClusterType): AutoBackupFormType => { - const backup = db.spec.backup - ? adaptPolicy({ - metadata: { - name: db.metadata.name, - uid: db.metadata.uid - }, - spec: { - retention: { - ttl: db.spec.backup.retentionPeriod - }, - schedule: { - datafile: { - cronExpression: db.spec.backup.cronExpression, - enable: db.spec.backup.enabled - } - } - } - }) - : { - start: false, - hour: '18', - minute: '00', - week: [], - type: 'day', - saveTime: 7, - saveType: 'd' - }; + const backup = + db.spec?.backup && db.spec?.backup?.cronExpression + ? adaptPolicy(db.spec.backup) + : { + start: false, + hour: '18', + minute: '00', + week: [], + type: 'day' as AutoBackupType, + saveTime: 7, + saveType: 'd' + }; return backup; }; +export const convertBackupFormToSpec = (data: { + autoBackup?: AutoBackupFormType; + dbType: DBType; +}): KbPgClusterType['spec']['backup'] => { + const cron = (() => { + if (data.autoBackup?.type === 'week') { + if (!data.autoBackup?.week?.length) { + throw new Error('Week is empty'); + } + return `${data.autoBackup.minute} ${data.autoBackup.hour} * * ${data.autoBackup.week.join( + ',' + )}`; + } + if (data.autoBackup?.type === 'day') { + return `${data.autoBackup.minute} ${data.autoBackup.hour} * * *`; + } + return `${data.autoBackup?.minute} * * * *`; + })(); + + return { + enabled: data.autoBackup?.start ?? false, + cronExpression: convertCronTime(cron, -8), + method: DBBackupMethodNameMap[data.dbType], + retentionPeriod: `${data.autoBackup?.saveTime}${data.autoBackup?.saveType}`, + repoName: '', + pitrEnabled: false + }; +}; + export const adaptDBForm = (db: DBDetailType): DBEditType => { const keys: Record = { dbType: 1, @@ -150,7 +163,9 @@ export const adaptDBForm = (db: DBDetailType): DBEditType => { memory: 1, replicas: 1, storage: 1, - labels: 1 + labels: 1, + autoBackup: 1, + terminationPolicy: 1 }; const form: any = {}; @@ -220,7 +235,7 @@ export const adaptBackup = (backup: BackupCRItemType): BackupItemType => { }; }; -export const adaptPolicy = (policy: KubeBlockBackupPolicyType): AutoBackupFormType => { +export const adaptPolicy = (policy: KbPgClusterType['spec']['backup']): AutoBackupFormType => { function parseDate(str: string) { const regex = /(\d+)([a-zA-Z]+)/; const matches = str.match(regex); @@ -234,6 +249,7 @@ export const adaptPolicy = (policy: KubeBlockBackupPolicyType): AutoBackupFormTy return { number: 7, unit: 'd' }; } + function parseCron(str: string) { const cronFields = convertCronTime(str, 8).split(' '); const minuteField = cronFields[0]; @@ -249,7 +265,6 @@ export const adaptPolicy = (policy: KubeBlockBackupPolicyType): AutoBackupFormTy type: 'week' }; } - console.log(minuteField, hourField, weekField); // every day if (hourField !== '*') { @@ -279,12 +294,12 @@ export const adaptPolicy = (policy: KubeBlockBackupPolicyType): AutoBackupFormTy }; } - const { number: saveTime, unit: saveType } = parseDate(policy.spec.retention.ttl); - const { hour, minute, week, type } = parseCron(policy.spec.schedule.datafile.cronExpression); + const { number: saveTime, unit: saveType } = parseDate(policy.retentionPeriod); + const { hour, minute, week, type } = parseCron(policy?.cronExpression ?? '0 0 * * *'); return { - start: policy.spec.schedule.datafile.enable, - type, + start: policy.enabled, + type: type as AutoBackupType, week, hour, minute, diff --git a/frontend/providers/dbprovider/src/utils/json2Yaml.ts b/frontend/providers/dbprovider/src/utils/json2Yaml.ts index 88fb8333efd..6c581cd1491 100644 --- a/frontend/providers/dbprovider/src/utils/json2Yaml.ts +++ b/frontend/providers/dbprovider/src/utils/json2Yaml.ts @@ -11,13 +11,14 @@ import { } from '@/constants/db'; import { StorageClassName } from '@/store/env'; import type { BackupItemType, DBDetailType, DBEditType, DBType } from '@/types/db'; -import { DumpForm, MigrateForm } from '@/types/migrate'; +import { MigrateForm } from '@/types/migrate'; import { encodeToHex, formatTime, str2Num } from '@/utils/tools'; import dayjs from 'dayjs'; import yaml from 'js-yaml'; import { getUserNamespace } from './user'; import { V1StatefulSet } from '@kubernetes/client-node'; import { customAlphabet } from 'nanoid'; + const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5); /** @@ -110,7 +111,7 @@ export const json2CreateCluster = (data: DBEditType, backupInfo?: BackupItemType ] } ], - terminationPolicy, + terminationPolicy: data.terminationPolicy, tolerations: [] } } @@ -153,7 +154,7 @@ export const json2CreateCluster = (data: DBEditType, backupInfo?: BackupItemType ] } ], - terminationPolicy, + terminationPolicy: data.terminationPolicy, tolerations: [] } } @@ -199,7 +200,7 @@ export const json2CreateCluster = (data: DBEditType, backupInfo?: BackupItemType ] } ], - terminationPolicy, + terminationPolicy: data.terminationPolicy, tolerations: [] } } @@ -280,7 +281,7 @@ export const json2CreateCluster = (data: DBEditType, backupInfo?: BackupItemType : {}) } ], - terminationPolicy, + terminationPolicy: data.terminationPolicy, tolerations: [] } } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx index f8f87d783ca..ff37ed8b9f8 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx @@ -145,6 +145,9 @@ const DevboxList = ({ height={'20px'} alt={item.id} src={`/images/${item.runtimeType}.svg`} + onError={(e) => { + e.currentTarget.src = '/images/custom.svg' + }} /> {item.name} @@ -376,6 +379,7 @@ const DevboxList = ({ devbox={delDevbox} onClose={() => setDelDevbox(null)} onSuccess={refetchDevboxList} + refetchDevboxList={refetchDevboxList} /> )} {!!onOpenRelease && !!currentDevboxListItem && ( diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx index 31e33b8597c..e59705e3f6d 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx @@ -77,6 +77,9 @@ const BasicInfo = () => { height={'20px'} alt={devboxDetail?.runtimeType} src={`/images/${devboxDetail?.runtimeType}.svg`} + onError={(e) => { + e.currentTarget.src = '/images/custom.svg' + }} /> diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx index 659fb89966d..7715a12ab2e 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx @@ -15,6 +15,7 @@ import IDEButton from '@/components/IDEButton' import DelModal from '@/components/modals/DelModal' import DevboxStatusTag from '@/components/DevboxStatusTag' import { sealosApp } from 'sealos-desktop-sdk/app' +import { useQuery } from '@tanstack/react-query' const Header = ({ refetchDevboxDetail, @@ -29,12 +30,18 @@ const Header = ({ const t = useTranslations() const { message: toast } = useMessage() - const { devboxDetail } = useDevboxStore() + const { devboxDetail, setDevboxList } = useDevboxStore() const { screenWidth, setLoading } = useGlobalStore() const [delDevbox, setDelDevbox] = useState(null) const isBigButton = useMemo(() => screenWidth > 1000, [screenWidth]) + const { refetch: refetchDevboxList } = useQuery(['devboxListQuery'], setDevboxList, { + onSettled(res) { + if (!res) return + } + }) + const handlePauseDevbox = useCallback( async (devbox: DevboxDetailType) => { try { @@ -167,6 +174,7 @@ const Header = ({ isBigButton={isBigButton} leftButtonProps={{ height: '40px', + width: '96px', borderWidth: '1 0 1 1', bg: 'white', color: 'grayModern.600' @@ -275,6 +283,7 @@ const Header = ({ setDelDevbox(null) router.push('/') }} + refetchDevboxList={refetchDevboxList} /> )} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx index de64902cb8b..6c9b24e6651 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx @@ -49,7 +49,7 @@ const Version = () => { { refetchInterval: devboxVersionList.length > 0 && - devboxVersionList[0].status.value !== DevboxReleaseStatusEnum.Success + devboxVersionList[0].status.value === DevboxReleaseStatusEnum.Pending ? 3000 : false, onSettled() { @@ -131,6 +131,18 @@ const Version = () => { title: t('delete_successful'), status: 'success' }) + let retryCount = 0 + const maxRetries = 3 + const retryInterval = 3000 + + const retry = async () => { + if (retryCount < maxRetries) { + await new Promise((resolve) => setTimeout(resolve, retryInterval)) + await refetch() + retryCount++ + } + } + retry() } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('delete_failed'), @@ -140,8 +152,9 @@ const Version = () => { } setIsLoading(false) }, - [setIsLoading, toast, t] + [setIsLoading, toast, t, refetch] ) + const columns: { title: string dataIndex?: keyof DevboxVersionListItemType diff --git a/frontend/providers/devbox/components/modals/DelModal.tsx b/frontend/providers/devbox/components/modals/DelModal.tsx index 31029de0e32..5c84db8d456 100644 --- a/frontend/providers/devbox/components/modals/DelModal.tsx +++ b/frontend/providers/devbox/components/modals/DelModal.tsx @@ -24,11 +24,13 @@ import { DevboxDetailType, DevboxListItemType } from '@/types/devbox' const DelModal = ({ devbox, onClose, + refetchDevboxList, onSuccess }: { devbox: DevboxListItemType | DevboxDetailType onClose: () => void onSuccess: () => void + refetchDevboxList: () => void }) => { const t = useTranslations() const { message: toast } = useMessage() @@ -48,6 +50,19 @@ const DelModal = ({ }) onSuccess() onClose() + + let retryCount = 0 + const maxRetries = 3 + const retryInterval = 3000 + + const retry = async () => { + if (retryCount < maxRetries) { + await new Promise((resolve) => setTimeout(resolve, retryInterval)) + await refetchDevboxList() + retryCount++ + } + } + retry() } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('delete_failed'), @@ -56,7 +71,7 @@ const DelModal = ({ console.error(error) } setLoading(false) - }, [devbox.name, removeDevboxIDE, toast, t, onSuccess, onClose]) + }, [devbox.name, removeDevboxIDE, toast, t, onSuccess, onClose, refetchDevboxList]) return ( diff --git a/frontend/providers/devbox/stores/ide.ts b/frontend/providers/devbox/stores/ide.ts index 953f0deedc4..553a2ca02e2 100644 --- a/frontend/providers/devbox/stores/ide.ts +++ b/frontend/providers/devbox/stores/ide.ts @@ -6,7 +6,7 @@ export type IDEType = 'vscode' | 'cursor' | 'vscodeInsiders' | 'windsurf' type State = { devboxIDEList: { ide: IDEType; devboxName: string }[] - getDevboxIDEByDevboxName: (devboxName: string) => IDEType | undefined + getDevboxIDEByDevboxName: (devboxName: string) => IDEType addDevboxIDE: (ide: IDEType, devboxName: string) => void updateDevboxIDE: (ide: IDEType, devboxName: string) => void removeDevboxIDE: (devboxName: string) => void @@ -18,7 +18,7 @@ export const useIDEStore = create()( immer((set, get) => ({ devboxIDEList: [], getDevboxIDEByDevboxName(devboxName: string) { - return get().devboxIDEList.find((item) => item.devboxName === devboxName)?.ide + return get().devboxIDEList.find((item) => item.devboxName === devboxName)?.ide || 'cursor' }, addDevboxIDE(ide: IDEType, devboxName: string) { set((state) => { @@ -30,6 +30,8 @@ export const useIDEStore = create()( const item = state.devboxIDEList.find((item) => item.devboxName === devboxName) if (item) { item.ide = ide + } else { + state.devboxIDEList.push({ ide, devboxName }) } }) }, diff --git a/service/aiproxy/common/balance/sealos.go b/service/aiproxy/common/balance/sealos.go index 16ab8577911..872f69fb995 100644 --- a/service/aiproxy/common/balance/sealos.go +++ b/service/aiproxy/common/balance/sealos.go @@ -14,9 +14,9 @@ import ( "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/env" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/redis/go-redis/v9" "github.com/shopspring/decimal" + log "github.com/sirupsen/logrus" ) const ( @@ -98,6 +98,7 @@ type sealosCache struct { Balance int64 `redis:"b"` } +//nolint:gosec func cacheSetGroupBalance(ctx context.Context, group string, balance int64, userUID string) error { if !common.RedisEnabled || !sealosRedisCacheEnable { return nil @@ -165,7 +166,7 @@ func (s *Sealos) getGroupRemainBalance(ctx context.Context, group string) (float return decimal.NewFromInt(cache.Balance).Div(decimalBalancePrecision).InexactFloat64(), newSealosPostGroupConsumer(s.accountURL, group, cache.UserUID, cache.Balance), nil } else if err != nil && !errors.Is(err, redis.Nil) { - logger.SysErrorf("get group (%s) balance cache failed: %s", group, err) + log.Errorf("get group (%s) balance cache failed: %s", group, err) } balance, userUID, err := s.fetchBalanceFromAPI(ctx, group) @@ -174,7 +175,7 @@ func (s *Sealos) getGroupRemainBalance(ctx context.Context, group string) (float } if err := cacheSetGroupBalance(ctx, group, balance, userUID); err != nil { - logger.SysErrorf("set group (%s) balance cache failed: %s", group, err) + log.Errorf("set group (%s) balance cache failed: %s", group, err) } return decimal.NewFromInt(balance).Div(decimalBalancePrecision).InexactFloat64(), @@ -237,7 +238,7 @@ func (s *SealosPostGroupConsumer) PostGroupConsume(ctx context.Context, tokenNam amount := s.calculateAmount(usage) if err := cacheDecreaseGroupBalance(ctx, s.group, amount.IntPart()); err != nil { - logger.SysErrorf("decrease group (%s) balance cache failed: %s", s.group, err) + log.Errorf("decrease group (%s) balance cache failed: %s", s.group, err) } if err := s.postConsume(ctx, amount.IntPart(), tokenName); err != nil { diff --git a/service/aiproxy/common/client/init.go b/service/aiproxy/common/client/init.go index 37b040c441f..18a45c9d70f 100644 --- a/service/aiproxy/common/client/init.go +++ b/service/aiproxy/common/client/init.go @@ -7,7 +7,7 @@ import ( "time" "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" ) var ( @@ -18,10 +18,10 @@ var ( func Init() { if config.UserContentRequestProxy != "" { - logger.SysLog(fmt.Sprintf("using %s as proxy to fetch user content", config.UserContentRequestProxy)) + log.Info(fmt.Sprintf("using %s as proxy to fetch user content", config.UserContentRequestProxy)) proxyURL, err := url.Parse(config.UserContentRequestProxy) if err != nil { - logger.FatalLog("USER_CONTENT_REQUEST_PROXY set but invalid: " + config.UserContentRequestProxy) + log.Fatal("USER_CONTENT_REQUEST_PROXY set but invalid: " + config.UserContentRequestProxy) } transport := &http.Transport{ Proxy: http.ProxyURL(proxyURL), @@ -35,10 +35,10 @@ func Init() { } var transport http.RoundTripper if config.RelayProxy != "" { - logger.SysLog(fmt.Sprintf("using %s as api relay proxy", config.RelayProxy)) + log.Info(fmt.Sprintf("using %s as api relay proxy", config.RelayProxy)) proxyURL, err := url.Parse(config.RelayProxy) if err != nil { - logger.FatalLog("USER_CONTENT_REQUEST_PROXY set but invalid: " + config.UserContentRequestProxy) + log.Fatal("USER_CONTENT_REQUEST_PROXY set but invalid: " + config.UserContentRequestProxy) } transport = &http.Transport{ Proxy: http.ProxyURL(proxyURL), diff --git a/service/aiproxy/common/color.go b/service/aiproxy/common/color.go new file mode 100644 index 00000000000..476015b25da --- /dev/null +++ b/service/aiproxy/common/color.go @@ -0,0 +1,20 @@ +package common + +import ( + "os" + "sync" + + "github.com/mattn/go-isatty" +) + +var ( + needColor bool + needColorOnce sync.Once +) + +func NeedColor() bool { + needColorOnce.Do(func() { + needColor = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) + }) + return needColor +} diff --git a/service/aiproxy/common/config/config.go b/service/aiproxy/common/config/config.go index 94991567cd6..d68f359b1c8 100644 --- a/service/aiproxy/common/config/config.go +++ b/service/aiproxy/common/config/config.go @@ -2,6 +2,7 @@ package config import ( "os" + "slices" "strconv" "sync" "sync/atomic" @@ -31,8 +32,18 @@ var ( retryTimes atomic.Int64 // 暂停服务 disableServe atomic.Bool + // log detail 存储时间(小时) + logDetailStorageHours int64 = 3 * 24 ) +func GetLogDetailStorageHours() int64 { + return atomic.LoadInt64(&logDetailStorageHours) +} + +func SetLogDetailStorageHours(hours int64) { + atomic.StoreInt64(&logDetailStorageHours, hours) +} + func GetDisableServe() bool { return disableServe.Load() } @@ -79,19 +90,6 @@ var RelayTimeout = env.Int("RELAY_TIMEOUT", 0) // unit is second var RateLimitKeyExpirationDuration = 20 * time.Minute -var ( - // 是否根据请求成功率禁用渠道,默认不开启 - EnableMetric = env.Bool("ENABLE_METRIC", false) - // 指标队列大小 - MetricQueueSize = env.Int("METRIC_QUEUE_SIZE", 10) - // 请求成功率阈值,默认80% - MetricSuccessRateThreshold = env.Float64("METRIC_SUCCESS_RATE_THRESHOLD", 0.8) - // 请求成功率指标队列大小 - MetricSuccessChanSize = env.Int("METRIC_SUCCESS_CHAN_SIZE", 1024) - // 请求失败率指标队列大小 - MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128) -) - var OnlyOneLogFile = env.Bool("ONLY_ONE_LOG_FILE", false) var ( @@ -141,6 +139,10 @@ func GetDefaultChannelModels() map[int][]string { } func SetDefaultChannelModels(models map[int][]string) { + for key, ms := range models { + slices.Sort(ms) + models[key] = slices.Compact(ms) + } defaultChannelModels.Store(models) } @@ -168,7 +170,7 @@ var ( func init() { geminiSafetySetting.Store("BLOCK_NONE") - geminiVersion.Store("v1") + geminiVersion.Store("v1beta") } func GetGeminiSafetySetting() string { @@ -186,3 +188,17 @@ func GetGeminiVersion() string { func SetGeminiVersion(version string) { geminiVersion.Store(version) } + +var billingEnabled atomic.Bool + +func init() { + billingEnabled.Store(true) +} + +func GetBillingEnabled() bool { + return billingEnabled.Load() +} + +func SetBillingEnabled(enabled bool) { + billingEnabled.Store(enabled) +} diff --git a/service/aiproxy/common/ctxkey/key.go b/service/aiproxy/common/ctxkey/key.go index c2adec1a4e6..488a856ca52 100644 --- a/service/aiproxy/common/ctxkey/key.go +++ b/service/aiproxy/common/ctxkey/key.go @@ -1,24 +1,13 @@ package ctxkey +type OriginalModelKey string + +const ( + OriginalModel OriginalModelKey = "original_model" +) + const ( - Config = "config" - Status = "status" - Channel = "channel" - ChannelID = "channel_id" - APIKey = "api_key" - SpecificChannelID = "specific_channel_id" - RequestModel = "request_model" - ConvertedRequest = "converted_request" - OriginalModel = "original_model" - Group = "group" - GroupQPM = "group_qpm" - ModelMapping = "model_mapping" - ChannelName = "channel_name" - TokenID = "token_id" - TokenName = "token_name" - TokenUsedAmount = "token_used_amount" - TokenQuota = "token_quota" - BaseURL = "base_url" - AvailableModels = "available_models" - KeyRequestBody = "key_request_body" + Channel = "channel" + Group = "group" + Token = "token" ) diff --git a/service/aiproxy/common/gin.go b/service/aiproxy/common/gin.go index f1027137533..113617b4d77 100644 --- a/service/aiproxy/common/gin.go +++ b/service/aiproxy/common/gin.go @@ -2,42 +2,47 @@ package common import ( "bytes" + "context" "fmt" "io" + "net/http" "github.com/gin-gonic/gin" json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" ) -func GetRequestBody(c *gin.Context) ([]byte, error) { - requestBody, ok := c.Get(ctxkey.KeyRequestBody) - if ok { +type RequestBodyKey struct{} + +func GetRequestBody(req *http.Request) ([]byte, error) { + requestBody := req.Context().Value(RequestBodyKey{}) + if requestBody != nil { return requestBody.([]byte), nil } var buf []byte var err error defer func() { - c.Request.Body.Close() + req.Body.Close() if err == nil { - c.Request.Body = io.NopCloser(bytes.NewBuffer(buf)) + req.Body = io.NopCloser(bytes.NewBuffer(buf)) } }() - if c.Request.ContentLength <= 0 || c.Request.Header.Get("Content-Type") != "application/json" { - buf, err = io.ReadAll(c.Request.Body) + if req.ContentLength <= 0 || req.Header.Get("Content-Type") != "application/json" { + buf, err = io.ReadAll(req.Body) } else { - buf = make([]byte, c.Request.ContentLength) - _, err = io.ReadFull(c.Request.Body, buf) + buf = make([]byte, req.ContentLength) + _, err = io.ReadFull(req.Body, buf) } if err != nil { return nil, fmt.Errorf("request body read failed: %w", err) } - c.Set(ctxkey.KeyRequestBody, buf) + ctx := req.Context() + bufCtx := context.WithValue(ctx, RequestBodyKey{}, buf) + *req = *req.WithContext(bufCtx) return buf, nil } -func UnmarshalBodyReusable(c *gin.Context, v any) error { - requestBody, err := GetRequestBody(c) +func UnmarshalBodyReusable(req *http.Request, v any) error { + requestBody, err := GetRequestBody(req) if err != nil { return err } diff --git a/service/aiproxy/common/helper/helper.go b/service/aiproxy/common/helper/helper.go index 3a8f55e58a5..8d882b60c68 100644 --- a/service/aiproxy/common/helper/helper.go +++ b/service/aiproxy/common/helper/helper.go @@ -3,13 +3,14 @@ package helper import ( "fmt" "strconv" + "time" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common/random" ) func GenRequestID() string { - return GetTimeString() + random.GetRandomNumberString(8) + return strconv.FormatInt(time.Now().UnixMilli(), 10) + random.GetRandomNumberString(4) } func GetResponseID(c *gin.Context) string { diff --git a/service/aiproxy/common/helper/time.go b/service/aiproxy/common/helper/time.go index 302746dbff9..757e56af23d 100644 --- a/service/aiproxy/common/helper/time.go +++ b/service/aiproxy/common/helper/time.go @@ -1,15 +1,9 @@ package helper import ( - "fmt" "time" ) func GetTimestamp() int64 { return time.Now().Unix() } - -func GetTimeString() string { - now := time.Now() - return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9) -} diff --git a/service/aiproxy/common/image/image.go b/service/aiproxy/common/image/image.go index 4a768cef980..505d63f9414 100644 --- a/service/aiproxy/common/image/image.go +++ b/service/aiproxy/common/image/image.go @@ -58,7 +58,7 @@ func GetImageSizeFromURL(url string) (width int, height int, err error) { return img.Width, img.Height, nil } -func GetImageFromURL(url string) (string, string, error) { +func GetImageFromURL(ctx context.Context, url string) (string, string, error) { // Check if the URL is a data URL matches := dataURLPattern.FindStringSubmatch(url) if len(matches) == 3 { @@ -66,7 +66,7 @@ func GetImageFromURL(url string) (string, string, error) { return "image/" + matches[1], matches[2], nil } - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", "", err } diff --git a/service/aiproxy/common/init.go b/service/aiproxy/common/init.go index 7d26db36d6c..fa2826e407a 100644 --- a/service/aiproxy/common/init.go +++ b/service/aiproxy/common/init.go @@ -2,11 +2,10 @@ package common import ( "flag" - "log" "os" "path/filepath" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" ) var ( @@ -32,6 +31,5 @@ func Init() { log.Fatal(err) } } - logger.LogDir = *LogDir } } diff --git a/service/aiproxy/common/logger/constants.go b/service/aiproxy/common/logger/constants.go deleted file mode 100644 index 49df31ec715..00000000000 --- a/service/aiproxy/common/logger/constants.go +++ /dev/null @@ -1,3 +0,0 @@ -package logger - -var LogDir string diff --git a/service/aiproxy/common/logger/logger.go b/service/aiproxy/common/logger/logger.go deleted file mode 100644 index ae777610f94..00000000000 --- a/service/aiproxy/common/logger/logger.go +++ /dev/null @@ -1,128 +0,0 @@ -package logger - -import ( - "context" - "fmt" - "io" - "log" - "os" - "path/filepath" - "sync" - "time" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/helper" -) - -const ( - loggerDEBUG = "DEBUG" - loggerINFO = "INFO" - loggerWarn = "WARN" - loggerError = "ERR" -) - -var setupLogOnce sync.Once - -func SetupLogger() { - setupLogOnce.Do(func() { - if LogDir != "" { - var logPath string - if config.OnlyOneLogFile { - logPath = filepath.Join(LogDir, "aiproxy.log") - } else { - logPath = filepath.Join(LogDir, fmt.Sprintf("aiproxy-%s.log", time.Now().Format("20060102"))) - } - fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - log.Fatal("failed to open log file") - } - gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) - gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) - } - }) -} - -func SysLog(s string) { - t := time.Now() - _, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) -} - -func SysLogf(format string, a ...any) { - SysLog(fmt.Sprintf(format, a...)) -} - -func SysDebug(s string) { - if config.DebugEnabled { - SysLog(s) - } -} - -func SysDebugf(format string, a ...any) { - if config.DebugEnabled { - SysLogf(format, a...) - } -} - -func SysError(s string) { - t := time.Now() - _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) -} - -func SysErrorf(format string, a ...any) { - SysError(fmt.Sprintf(format, a...)) -} - -func Debug(ctx context.Context, msg string) { - if config.DebugEnabled { - logHelper(ctx, loggerDEBUG, msg) - } -} - -func Info(ctx context.Context, msg string) { - logHelper(ctx, loggerINFO, msg) -} - -func Warn(ctx context.Context, msg string) { - logHelper(ctx, loggerWarn, msg) -} - -func Error(ctx context.Context, msg string) { - logHelper(ctx, loggerError, msg) -} - -func Debugf(ctx context.Context, format string, a ...any) { - Debug(ctx, fmt.Sprintf(format, a...)) -} - -func Infof(ctx context.Context, format string, a ...any) { - Info(ctx, fmt.Sprintf(format, a...)) -} - -func Warnf(ctx context.Context, format string, a ...any) { - Warn(ctx, fmt.Sprintf(format, a...)) -} - -func Errorf(ctx context.Context, format string, a ...any) { - Error(ctx, fmt.Sprintf(format, a...)) -} - -func logHelper(ctx context.Context, level string, msg string) { - writer := gin.DefaultErrorWriter - if level == loggerINFO { - writer = gin.DefaultWriter - } - id := ctx.Value(helper.RequestIDKey) - if id == nil { - id = helper.GenRequestID() - } - now := time.Now() - _, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg) - SetupLogger() -} - -func FatalLog(v ...any) { - t := time.Now() - _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v) - os.Exit(1) -} diff --git a/service/aiproxy/common/network/ip.go b/service/aiproxy/common/network/ip.go index cb335ad642a..dcbe49bed0d 100644 --- a/service/aiproxy/common/network/ip.go +++ b/service/aiproxy/common/network/ip.go @@ -1,12 +1,9 @@ package network import ( - "context" "fmt" "net" "strings" - - "github.com/labring/sealos/service/aiproxy/common/logger" ) func splitSubnets(subnets string) []string { @@ -25,13 +22,12 @@ func isValidSubnet(subnet string) error { return nil } -func isIPInSubnet(ctx context.Context, ip string, subnet string) bool { +func isIPInSubnet(ip string, subnet string) (bool, error) { _, ipNet, err := net.ParseCIDR(subnet) if err != nil { - logger.Errorf(ctx, "failed to parse subnet: %s", err.Error()) - return false + return false, fmt.Errorf("failed to parse subnet: %w", err) } - return ipNet.Contains(net.ParseIP(ip)) + return ipNet.Contains(net.ParseIP(ip)), nil } func IsValidSubnets(subnets string) error { @@ -43,11 +39,13 @@ func IsValidSubnets(subnets string) error { return nil } -func IsIPInSubnets(ctx context.Context, ip string, subnets string) bool { +func IsIPInSubnets(ip string, subnets string) (bool, error) { for _, subnet := range splitSubnets(subnets) { - if isIPInSubnet(ctx, ip, subnet) { - return true + if ok, err := isIPInSubnet(ip, subnet); err != nil { + return false, err + } else if ok { + return true, nil } } - return false + return false, nil } diff --git a/service/aiproxy/common/network/ip_test.go b/service/aiproxy/common/network/ip_test.go index 24a92d74f38..1194957b8d8 100644 --- a/service/aiproxy/common/network/ip_test.go +++ b/service/aiproxy/common/network/ip_test.go @@ -1,19 +1,25 @@ package network import ( - "context" "testing" "github.com/smartystreets/goconvey/convey" ) func TestIsIpInSubnet(t *testing.T) { - ctx := context.Background() ip1 := "192.168.0.5" ip2 := "125.216.250.89" subnet := "192.168.0.0/24" convey.Convey("TestIsIpInSubnet", t, func() { - convey.So(isIPInSubnet(ctx, ip1, subnet), convey.ShouldBeTrue) - convey.So(isIPInSubnet(ctx, ip2, subnet), convey.ShouldBeFalse) + if ok, err := isIPInSubnet(ip1, subnet); err != nil { + t.Errorf("failed to check ip in subnet: %s", err) + } else { + convey.So(ok, convey.ShouldBeTrue) + } + if ok, err := isIPInSubnet(ip2, subnet); err != nil { + t.Errorf("failed to check ip in subnet: %s", err) + } else { + convey.So(ok, convey.ShouldBeFalse) + } }) } diff --git a/service/aiproxy/common/random/main.go b/service/aiproxy/common/random/main.go index 79ba35e39a7..dcde39f85e8 100644 --- a/service/aiproxy/common/random/main.go +++ b/service/aiproxy/common/random/main.go @@ -10,7 +10,7 @@ import ( func GetUUID() string { code := uuid.New().String() - code = strings.Replace(code, "-", "", -1) + code = strings.ReplaceAll(code, "-", "") return code } @@ -19,6 +19,7 @@ const ( keyNumbers = "0123456789" ) +//nolint:gosec func GenerateKey() string { key := make([]byte, 48) for i := 0; i < 16; i++ { @@ -35,6 +36,7 @@ func GenerateKey() string { return conv.BytesToString(key) } +//nolint:gosec func GetRandomString(length int) string { key := make([]byte, length) for i := 0; i < length; i++ { @@ -43,6 +45,7 @@ func GetRandomString(length int) string { return conv.BytesToString(key) } +//nolint:gosec func GetRandomNumberString(length int) string { key := make([]byte, length) for i := 0; i < length; i++ { @@ -52,6 +55,8 @@ func GetRandomNumberString(length int) string { } // RandRange returns a random number between min and max (max is not included) +// +//nolint:gosec func RandRange(_min, _max int) int { return _min + rand.IntN(_max-_min) } diff --git a/service/aiproxy/common/redis.go b/service/aiproxy/common/redis.go index 685d7dd671c..30a7ad1b85b 100644 --- a/service/aiproxy/common/redis.go +++ b/service/aiproxy/common/redis.go @@ -5,8 +5,8 @@ import ( "os" "time" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/redis/go-redis/v9" + log "github.com/sirupsen/logrus" ) var ( @@ -17,14 +17,14 @@ var ( // InitRedisClient This function is called after init() func InitRedisClient() (err error) { if os.Getenv("REDIS_CONN_STRING") == "" { - logger.SysLog("REDIS_CONN_STRING not set, redis is not enabled") + log.Info("REDIS_CONN_STRING not set, redis is not enabled") return nil } RedisEnabled = true - logger.SysLog("redis is enabled") + log.Info("redis is enabled") opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING")) if err != nil { - logger.FatalLog("failed to parse redis connection string: " + err.Error()) + log.Fatal("failed to parse redis connection string: " + err.Error()) } RDB = redis.NewClient(opt) @@ -32,9 +32,6 @@ func InitRedisClient() (err error) { defer cancel() _, err = RDB.Ping(ctx).Result() - if err != nil { - logger.FatalLog("redis ping test failed: " + err.Error()) - } return err } diff --git a/service/aiproxy/common/render/render.go b/service/aiproxy/common/render/render.go index 9a7d0fe0d68..ff4e8f07b77 100644 --- a/service/aiproxy/common/render/render.go +++ b/service/aiproxy/common/render/render.go @@ -1,6 +1,7 @@ package render import ( + "errors" "fmt" "strings" @@ -10,7 +11,15 @@ import ( "github.com/labring/sealos/service/aiproxy/common/conv" ) +var stdjson = json.ConfigCompatibleWithStandardLibrary + func StringData(c *gin.Context, str string) { + if len(c.Errors) > 0 { + return + } + if c.IsAborted() { + return + } str = strings.TrimPrefix(str, "data:") // str = strings.TrimSuffix(str, "\r") c.Render(-1, common.CustomEvent{Data: "data: " + strings.TrimSpace(str)}) @@ -18,7 +27,13 @@ func StringData(c *gin.Context, str string) { } func ObjectData(c *gin.Context, object any) error { - jsonData, err := json.Marshal(object) + if len(c.Errors) > 0 { + return c.Errors.Last() + } + if c.IsAborted() { + return errors.New("context aborted") + } + jsonData, err := stdjson.Marshal(object) if err != nil { return fmt.Errorf("error marshalling object: %w", err) } diff --git a/service/aiproxy/controller/channel-billing.go b/service/aiproxy/controller/channel-billing.go index 5e5933b5c3b..bf6247be234 100644 --- a/service/aiproxy/controller/channel-billing.go +++ b/service/aiproxy/controller/channel-billing.go @@ -1,336 +1,73 @@ package controller import ( - "context" - "errors" "fmt" - "io" "net/http" "strconv" "time" - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/balance" - "github.com/labring/sealos/service/aiproxy/common/client" "github.com/labring/sealos/service/aiproxy/common/ctxkey" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/channeltype" + log "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" ) // https://github.com/labring/sealos/service/aiproxy/issues/79 -type OpenAISubscriptionResponse struct { - Object string `json:"object"` - HasPaymentMethod bool `json:"has_payment_method"` - SoftLimitUSD float64 `json:"soft_limit_usd"` - HardLimitUSD float64 `json:"hard_limit_usd"` - SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` - AccessUntil int64 `json:"access_until"` -} - -type OpenAIUsageDailyCost struct { - LineItems []struct { - Name string `json:"name"` - Cost float64 `json:"cost"` - } - Timestamp float64 `json:"timestamp"` -} - -type OpenAICreditGrants struct { - Object string `json:"object"` - TotalGranted float64 `json:"total_granted"` - TotalUsed float64 `json:"total_used"` - TotalAvailable float64 `json:"total_available"` -} - -type OpenAIUsageResponse struct { - Object string `json:"object"` - // DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"` - TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar -} - -type OpenAISBUsageResponse struct { - Data *struct { - Credit string `json:"credit"` - } `json:"data"` - Msg string `json:"msg"` -} - -type AIProxyUserOverviewResponse struct { - Message string `json:"message"` - ErrorCode int `json:"error_code"` - Data struct { - TotalPoints float64 `json:"totalPoints"` - } `json:"data"` - Success bool `json:"success"` -} - -type API2GPTUsageResponse struct { - Object string `json:"object"` - TotalGranted float64 `json:"total_granted"` - TotalUsed float64 `json:"total_used"` - TotalRemaining float64 `json:"total_remaining"` -} - -type APGC2DGPTUsageResponse struct { - // Grants interface{} `json:"grants"` - Object string `json:"object"` - TotalAvailable float64 `json:"total_available"` - TotalGranted float64 `json:"total_granted"` - TotalUsed float64 `json:"total_used"` -} - -type SiliconFlowUsageResponse struct { - Message string `json:"message"` - Data struct { - ID string `json:"id"` - Name string `json:"name"` - Image string `json:"image"` - Email string `json:"email"` - Balance string `json:"balance"` - Status string `json:"status"` - Introduction string `json:"introduction"` - Role string `json:"role"` - ChargeBalance string `json:"chargeBalance"` - TotalBalance string `json:"totalBalance"` - Category string `json:"category"` - IsAdmin bool `json:"isAdmin"` - } `json:"data"` - Code int `json:"code"` - Status bool `json:"status"` -} - -// GetAuthHeader get auth header -func GetAuthHeader(token string) http.Header { - h := http.Header{} - h.Add("Authorization", "Bearer "+token) - return h -} - -func GetResponseBody(method, url string, _ *model.Channel, headers http.Header) ([]byte, error) { - req, err := http.NewRequestWithContext(context.Background(), method, url, nil) - if err != nil { - return nil, err - } - for k := range headers { - req.Header.Add(k, headers.Get(k)) - } - res, err := client.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("status code: %d", res.StatusCode) - } - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - return body, nil -} - -func updateChannelCloseAIBalance(channel *model.Channel) (float64, error) { - url := channel.BaseURL + "/dashboard/billing/credit_grants" - body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err - } - response := OpenAICreditGrants{} - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - channel.UpdateBalance(response.TotalAvailable) - return response.TotalAvailable, nil -} - -func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { - url := "https://api.openai-sb.com/sb-api/user/status?api_key=" + channel.Key - body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err - } - response := OpenAISBUsageResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - if response.Data == nil { - return 0, errors.New(response.Msg) - } - balance, err := strconv.ParseFloat(response.Data.Credit, 64) - if err != nil { - return 0, err - } - channel.UpdateBalance(balance) - return balance, nil -} - -func updateChannelAIProxyBalance(channel *model.Channel) (float64, error) { - url := "https://aiproxy.io/api/report/getUserOverview" - headers := http.Header{} - headers.Add("Api-Key", channel.Key) - body, err := GetResponseBody("GET", url, channel, headers) - if err != nil { - return 0, err - } - response := AIProxyUserOverviewResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - if !response.Success { - return 0, fmt.Errorf("code: %d, message: %s", response.ErrorCode, response.Message) - } - channel.UpdateBalance(response.Data.TotalPoints) - return response.Data.TotalPoints, nil -} - -func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) { - url := "https://api.api2gpt.com/dashboard/billing/credit_grants" - body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err - } - response := API2GPTUsageResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - channel.UpdateBalance(response.TotalRemaining) - return response.TotalRemaining, nil -} - -func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) { - url := "https://api.aigc2d.com/dashboard/billing/credit_grants" - body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err - } - response := APGC2DGPTUsageResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - channel.UpdateBalance(response.TotalAvailable) - return response.TotalAvailable, nil -} - -func updateChannelSiliconFlowBalance(channel *model.Channel) (float64, error) { - url := "https://api.siliconflow.cn/v1/user/info" - body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err - } - response := SiliconFlowUsageResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - if response.Code != 20000 { - return 0, fmt.Errorf("code: %d, message: %s", response.Code, response.Message) - } - balance, err := strconv.ParseFloat(response.Data.Balance, 64) - if err != nil { - return 0, err - } - channel.UpdateBalance(balance) - return balance, nil -} - func updateChannelBalance(channel *model.Channel) (float64, error) { - baseURL := channeltype.ChannelBaseURLs[channel.Type] - if channel.BaseURL == "" { - channel.BaseURL = baseURL - } - switch channel.Type { - case channeltype.OpenAI: - baseURL = channel.BaseURL - case channeltype.Azure: - return 0, errors.New("尚未实现") - case channeltype.Custom: - baseURL = channel.BaseURL - case channeltype.CloseAI: - return updateChannelCloseAIBalance(channel) - case channeltype.OpenAISB: - return updateChannelOpenAISBBalance(channel) - case channeltype.AIProxy: - return updateChannelAIProxyBalance(channel) - case channeltype.API2GPT: - return updateChannelAPI2GPTBalance(channel) - case channeltype.AIGC2D: - return updateChannelAIGC2DBalance(channel) - case channeltype.SiliconFlow: - return updateChannelSiliconFlowBalance(channel) - default: - return 0, errors.New("尚未实现") - } - url := baseURL + "/v1/dashboard/billing/subscription" - - body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err - } - subscription := OpenAISubscriptionResponse{} - err = json.Unmarshal(body, &subscription) - if err != nil { - return 0, err - } - now := time.Now() - startDate := now.Format("2006-01") + "-01" - endDate := now.Format("2006-01-02") - if !subscription.HasPaymentMethod { - startDate = now.AddDate(0, 0, -100).Format("2006-01-02") - } - url = baseURL + "/v1/dashboard/billing/usage?start_date=" + startDate + "&end_date=" + endDate - body, err = GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) - if err != nil { - return 0, err + adaptorI, ok := channeltype.GetAdaptor(channel.Type) + if !ok { + return 0, fmt.Errorf("invalid channel type: %d", channel.Type) } - usage := OpenAIUsageResponse{} - err = json.Unmarshal(body, &usage) - if err != nil { - return 0, err + if getBalance, ok := adaptorI.(adaptor.GetBalance); ok { + balance, err := getBalance.GetBalance(channel) + if err != nil { + return 0, err + } + err = channel.UpdateBalance(balance) + if err != nil { + log.Errorf("failed to update channel %s(%d) balance: %s", channel.Name, channel.ID, err.Error()) + } + return balance, nil } - balance := subscription.HardLimitUSD - usage.TotalUsage/100 - channel.UpdateBalance(balance) - return balance, nil + return 0, fmt.Errorf("channel type %d does not support get balance", channel.Type) } func UpdateChannelBalance(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: err.Error(), }) return } channel, err := model.GetChannelByID(id, false) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: err.Error(), }) return } balance, err := updateChannelBalance(channel) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: err.Error(), }) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "balance": balance, + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: true, + Message: "", + Data: balance, }) } @@ -340,67 +77,50 @@ func updateAllChannelsBalance() error { return err } for _, channel := range channels { - if channel.Status != model.ChannelStatusEnabled { - continue - } - // TODO: support Azure - if channel.Type != channeltype.OpenAI && channel.Type != channeltype.Custom { - continue - } - balance, err := updateChannelBalance(channel) + _, err := updateChannelBalance(channel) if err != nil { continue } - // err is nil & balance <= 0 means quota is used up - if balance <= 0 { - _ = model.DisableChannelByID(channel.ID) - } - time.Sleep(time.Second) } return nil } func UpdateAllChannelsBalance(c *gin.Context) { - // err := updateAllChannelsBalance() - // if err != nil { - // c.JSON(http.StatusOK, gin.H{ - // "success": false, - // "message": err.Error(), - // }) - // return - // } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + err := updateAllChannelsBalance() + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) } func AutomaticallyUpdateChannels(frequency int) { for { time.Sleep(time.Duration(frequency) * time.Minute) - logger.SysLog("updating all channels") + log.Info("updating all channels") _ = updateAllChannelsBalance() - logger.SysLog("channels update done") + log.Info("channels update done") } } // subscription func GetSubscription(c *gin.Context) { - group := c.GetString(ctxkey.Group) - b, _, err := balance.Default.GetGroupRemainBalance(c, group) + group := c.MustGet(ctxkey.Group).(*model.GroupCache) + b, _, err := balance.Default.GetGroupRemainBalance(c, group.ID) if err != nil { - logger.Errorf(c, "get group (%s) balance failed: %s", group, err) - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": fmt.Sprintf("get group (%s) balance failed", group), + log.Errorf("get group (%s) balance failed: %s", group.ID, err) + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: fmt.Sprintf("get group (%s) balance failed", group.ID), }) return } - quota := c.GetFloat64(ctxkey.TokenQuota) + token := c.MustGet(ctxkey.Token).(*model.TokenCache) + quota := token.Quota if quota <= 0 { quota = b } - c.JSON(http.StatusOK, OpenAISubscriptionResponse{ + c.JSON(http.StatusOK, openai.SubscriptionResponse{ HardLimitUSD: quota / 7, SoftLimitUSD: b / 7, SystemHardLimitUSD: quota / 7, @@ -408,6 +128,6 @@ func GetSubscription(c *gin.Context) { } func GetUsage(c *gin.Context) { - usedAmount := c.GetFloat64(ctxkey.TokenUsedAmount) - c.JSON(http.StatusOK, OpenAIUsageResponse{TotalUsage: usedAmount / 7 * 100}) + token := c.MustGet(ctxkey.Token).(*model.TokenCache) + c.JSON(http.StatusOK, openai.UsageResponse{TotalUsage: token.UsedAmount / 7 * 100}) } diff --git a/service/aiproxy/controller/channel-test.go b/service/aiproxy/controller/channel-test.go index 45c8ee54356..5c431ff8792 100644 --- a/service/aiproxy/controller/channel-test.go +++ b/service/aiproxy/controller/channel-test.go @@ -1,236 +1,352 @@ package controller import ( - "bytes" "errors" "fmt" "io" + "math/rand/v2" "net/http" "net/http/httptest" "net/url" "slices" "strconv" "sync" + "sync/atomic" "time" - json "github.com/json-iterator/go" - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/common/helper" + "github.com/labring/sealos/service/aiproxy/common/render" "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" - "github.com/labring/sealos/service/aiproxy/monitor" - relay "github.com/labring/sealos/service/aiproxy/relay" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" - "github.com/labring/sealos/service/aiproxy/relay/controller" "github.com/labring/sealos/service/aiproxy/relay/meta" - relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" + log "github.com/sirupsen/logrus" ) -func buildTestRequest(model string) *relaymodel.GeneralOpenAIRequest { - if model == "" { - model = "gpt-3.5-turbo" +const channelTestRequestID = "channel-test" + +// testSingleModel tests a single model in the channel +func testSingleModel(channel *model.Channel, modelName string) (*model.ChannelTest, error) { + body, mode, err := utils.BuildRequest(modelName) + if err != nil { + return nil, err } - testRequest := &relaymodel.GeneralOpenAIRequest{ - MaxTokens: 2, - Model: model, + + w := httptest.NewRecorder() + newc, _ := gin.CreateTestContext(w) + newc.Request = &http.Request{ + Method: http.MethodPost, + URL: &url.URL{Path: utils.BuildModeDefaultPath(mode)}, + Body: io.NopCloser(body), + Header: make(http.Header), } - testMessage := relaymodel.Message{ - Role: "user", - Content: "hi", + newc.Set(string(helper.RequestIDKey), channelTestRequestID) + + meta := meta.NewMeta( + channel, + mode, + modelName, + meta.WithRequestID(channelTestRequestID), + meta.WithChannelTest(true), + ) + bizErr := relayHelper(meta, newc) + var respStr string + var code int + if bizErr == nil { + respStr = w.Body.String() + code = w.Code + } else { + respStr = bizErr.String() + code = bizErr.StatusCode } - testRequest.Messages = append(testRequest.Messages, testMessage) - return testRequest + + return channel.UpdateModelTest( + meta.RequestAt, + meta.OriginModelName, + meta.ActualModelName, + meta.Mode, + time.Since(meta.RequestAt).Seconds(), + bizErr == nil, + respStr, + code, + ) } -func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIRequest) (openaiErr *relaymodel.Error, err error) { - if len(channel.Models) == 0 { - channel.Models = config.GetDefaultChannelModels()[channel.Type] - if len(channel.Models) == 0 { - return nil, errors.New("no models") - } +//nolint:goconst +func TestChannel(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: err.Error(), + }) + return } - modelName := request.Model + + modelName := c.Param("model") if modelName == "" { - modelName = channel.Models[0] - } else if !slices.Contains(channel.Models, modelName) { - return nil, fmt.Errorf("model %s not supported", modelName) - } - if v, ok := channel.ModelMapping[modelName]; ok { - modelName = v - } - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = &http.Request{ - Method: http.MethodPost, - URL: &url.URL{Path: "/v1/chat/completions"}, - Body: nil, - Header: make(http.Header), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: "model is required", + }) + return } - c.Request.Header.Set("Authorization", "Bearer "+channel.Key) - c.Request.Header.Set("Content-Type", "application/json") - c.Set(ctxkey.Channel, channel.Type) - c.Set(ctxkey.BaseURL, channel.BaseURL) - c.Set(ctxkey.Config, channel.Config) - middleware.SetupContextForSelectedChannel(c, channel, "") - meta := meta.GetByContext(c) - apiType := channeltype.ToAPIType(channel.Type) - adaptor := relay.GetAdaptor(apiType) - if adaptor == nil { - return nil, fmt.Errorf("invalid api type: %d, adaptor is nil", apiType) - } - adaptor.Init(meta) - meta.OriginModelName, meta.ActualModelName = request.Model, modelName - request.Model = modelName - convertedRequest, err := adaptor.ConvertRequest(c, relaymode.ChatCompletions, request) + + channel, err := model.LoadChannelByID(id) if err != nil { - return nil, err + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: "channel not found", + }) + return } - jsonData, err := json.Marshal(convertedRequest) - if err != nil { - return nil, err + + if !slices.Contains(channel.Models, modelName) { + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: "model not supported by channel", + }) + return } - logger.SysLogf("testing channel #%d, request: \n%s", channel.ID, jsonData) - requestBody := bytes.NewBuffer(jsonData) - c.Request.Body = io.NopCloser(requestBody) - resp, err := adaptor.DoRequest(c, meta, requestBody) + + ct, err := testSingleModel(channel, modelName) if err != nil { - return nil, err + log.Errorf("failed to test channel %s(%d) model %s: %s", channel.Name, channel.ID, modelName, err.Error()) + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: fmt.Sprintf("failed to test channel %s(%d) model %s: %s", channel.Name, channel.ID, modelName, err.Error()), + }) + return } - if resp != nil && resp.StatusCode != http.StatusOK { - err := controller.RelayErrorHandler(resp, meta.Mode) - return &err.Error, errors.New(err.Error.Message) + + if c.Query("success_body") != "true" && ct.Success { + ct.Response = "" } - usage, respErr := adaptor.DoResponse(c, resp, meta) - if respErr != nil { - return &respErr.Error, errors.New(respErr.Error.Message) + + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: true, + Data: ct, + }) +} + +type testResult struct { + Data *model.ChannelTest `json:"data,omitempty"` + Message string `json:"message,omitempty"` + Success bool `json:"success"` +} + +func processTestResult(channel *model.Channel, modelName string, returnSuccess bool, successResponseBody bool) *testResult { + ct, err := testSingleModel(channel, modelName) + + e := &utils.UnsupportedModelTypeError{} + if errors.As(err, &e) { + log.Errorf("model %s not supported test: %s", modelName, err.Error()) + return nil } - if usage == nil { - return nil, errors.New("usage is nil") + + result := &testResult{ + Success: err == nil, } - result := w.Result() - // print result.Body - respBody, err := io.ReadAll(result.Body) if err != nil { - return nil, err + result.Message = fmt.Sprintf("failed to test channel %s(%d) model %s: %s", channel.Name, channel.ID, modelName, err.Error()) + return result + } + + if !ct.Success { + result.Data = ct + return result + } + + if !returnSuccess { + return nil + } + + if !successResponseBody { + ct.Response = "" } - logger.SysLogf("testing channel #%d, response: \n%s", channel.ID, respBody) - return nil, nil + result.Data = ct + return result } -func TestChannel(c *gin.Context) { +//nolint:goconst +//nolint:gosec +func TestChannelModels(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: err.Error(), }) return } - channel, err := model.GetChannelByID(id, false) + + channel, err := model.LoadChannelByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: "channel not found", }) return } - model := c.Query("model") - testRequest := buildTestRequest(model) - tik := time.Now() - _, err = testChannel(channel, testRequest) - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() - if err != nil { - milliseconds = 0 - } - go channel.UpdateResponseTime(milliseconds) - consumedTime := float64(milliseconds) / 1000.0 - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - "time": consumedTime, - "model": model, - }) - return + + returnSuccess := c.Query("return_success") == "true" + successResponseBody := c.Query("success_body") == "true" + isStream := c.Query("stream") == "true" + + if isStream { + common.SetEventStreamHeaders(c) } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "time": consumedTime, - "model": model, + + results := make([]*testResult, 0) + resultsMutex := sync.Mutex{} + hasError := atomic.Bool{} + + var wg sync.WaitGroup + semaphore := make(chan struct{}, 5) + + models := slices.Clone(channel.Models) + rand.Shuffle(len(models), func(i, j int) { + models[i], models[j] = models[j], models[i] }) -} -var ( - testAllChannelsLock sync.Mutex - testAllChannelsRunning = false -) + for _, modelName := range models { + wg.Add(1) + semaphore <- struct{}{} -func testChannels(onlyDisabled bool) error { - testAllChannelsLock.Lock() - if testAllChannelsRunning { - testAllChannelsLock.Unlock() - return errors.New("测试已在运行中") - } - testAllChannelsRunning = true - testAllChannelsLock.Unlock() - channels, err := model.GetAllChannels(onlyDisabled, false) - if err != nil { - return err - } - go func() { - for _, channel := range channels { - isChannelEnabled := channel.Status == model.ChannelStatusEnabled - tik := time.Now() - testRequest := buildTestRequest("") - openaiErr, err := testChannel(channel, testRequest) - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() - if isChannelEnabled && monitor.ShouldDisableChannel(openaiErr, -1) { - _ = model.DisableChannelByID(channel.ID) + go func(model string) { + defer wg.Done() + defer func() { <-semaphore }() + + result := processTestResult(channel, model, returnSuccess, successResponseBody) + if result == nil { + return + } + if !result.Success || (result.Data != nil && !result.Data.Success) { + hasError.Store(true) } - if !isChannelEnabled && monitor.ShouldEnableChannel(err, openaiErr) { - _ = model.EnableChannelByID(channel.ID) + resultsMutex.Lock() + if isStream { + err := render.ObjectData(c, result) + if err != nil { + log.Errorf("failed to render result: %s", err.Error()) + } + } else { + results = append(results, result) } - channel.UpdateResponseTime(milliseconds) - time.Sleep(time.Second * 1) + resultsMutex.Unlock() + }(modelName) + } + + wg.Wait() + + if !hasError.Load() { + err := model.ClearLastTestErrorAt(channel.ID) + if err != nil { + log.Errorf("failed to clear last test error at for channel %s(%d): %s", channel.Name, channel.ID, err.Error()) } - testAllChannelsLock.Lock() - testAllChannelsRunning = false - testAllChannelsLock.Unlock() - }() - return nil + } + + if !isStream { + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: true, + Data: results, + }) + } } -func TestChannels(c *gin.Context) { - onlyDisabled := c.Query("only_disabled") == "true" - err := testChannels(onlyDisabled) +//nolint:goconst +//nolint:gosec +func TestAllChannels(c *gin.Context) { + testDisabled := c.Query("test_disabled") == "true" + var channels []*model.Channel + var err error + if testDisabled { + channels, err = model.LoadChannels() + } else { + channels, err = model.LoadEnabledChannels() + } if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: false, + Message: err.Error(), }) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", + returnSuccess := c.Query("return_success") == "true" + successResponseBody := c.Query("success_body") == "true" + isStream := c.Query("stream") == "true" + + if isStream { + common.SetEventStreamHeaders(c) + } + + results := make([]*testResult, 0) + resultsMutex := sync.Mutex{} + hasErrorMap := make(map[int]*atomic.Bool) + + var wg sync.WaitGroup + semaphore := make(chan struct{}, 5) + + newChannels := slices.Clone(channels) + rand.Shuffle(len(newChannels), func(i, j int) { + newChannels[i], newChannels[j] = newChannels[j], newChannels[i] }) -} -func AutomaticallyTestChannels(frequency int) { - for { - time.Sleep(time.Duration(frequency) * time.Minute) - logger.SysLog("testing all channels") - err := testChannels(false) - if err != nil { - logger.SysLog("testing all channels failed: " + err.Error()) + for _, channel := range newChannels { + channelHasError := &atomic.Bool{} + hasErrorMap[channel.ID] = channelHasError + + models := slices.Clone(channel.Models) + rand.Shuffle(len(models), func(i, j int) { + models[i], models[j] = models[j], models[i] + }) + + for _, modelName := range models { + wg.Add(1) + semaphore <- struct{}{} + + go func(model string, ch *model.Channel, hasError *atomic.Bool) { + defer wg.Done() + defer func() { <-semaphore }() + + result := processTestResult(ch, model, returnSuccess, successResponseBody) + if result == nil { + return + } + if !result.Success || (result.Data != nil && !result.Data.Success) { + hasError.Store(true) + } + resultsMutex.Lock() + if isStream { + err := render.ObjectData(c, result) + if err != nil { + log.Errorf("failed to render result: %s", err.Error()) + } + } else { + results = append(results, result) + } + resultsMutex.Unlock() + }(modelName, channel, channelHasError) + } + } + + wg.Wait() + + for id, hasError := range hasErrorMap { + if !hasError.Load() { + err := model.ClearLastTestErrorAt(id) + if err != nil { + log.Errorf("failed to clear last test error at for channel %d: %s", id, err.Error()) + } } - logger.SysLog("channel test finished") + } + + if !isStream { + c.JSON(http.StatusOK, middleware.APIResponse{ + Success: true, + Data: results, + }) } } diff --git a/service/aiproxy/controller/channel.go b/service/aiproxy/controller/channel.go index dff792e721d..572a3a061d3 100644 --- a/service/aiproxy/controller/channel.go +++ b/service/aiproxy/controller/channel.go @@ -8,9 +8,15 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/channeltype" ) +func ChannelTypeNames(c *gin.Context) { + middleware.SuccessResponse(c, channeltype.ChannelNames) +} + func GetChannels(c *gin.Context) { p, _ := strconv.Atoi(c.Query("p")) p-- @@ -31,46 +37,29 @@ func GetChannels(c *gin.Context) { order := c.Query("order") channels, total, err := model.GetChannels(p*perPage, perPage, false, false, id, name, key, channelType, baseURL, order) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "channels": channels, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "channels": channels, + "total": total, }) } func GetAllChannels(c *gin.Context) { channels, err := model.GetAllChannels(false, false) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": channels, - }) + middleware.SuccessResponse(c, channels) } func AddChannels(c *gin.Context) { channels := make([]*AddChannelRequest, 0) err := c.ShouldBindJSON(&channels) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } _channels := make([]*model.Channel, 0, len(channels)) @@ -79,16 +68,10 @@ func AddChannels(c *gin.Context) { } err = model.BatchInsertChannels(_channels) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } func SearchChannels(c *gin.Context) { @@ -112,44 +95,27 @@ func SearchChannels(c *gin.Context) { order := c.Query("order") channels, total, err := model.SearchChannels(keyword, p*perPage, perPage, false, false, id, name, key, channelType, baseURL, order) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "channels": channels, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "channels": channels, + "total": total, }) } func GetChannel(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } channel, err := model.GetChannelByID(id, false) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": channel, - }) + middleware.SuccessResponse(c, channel) } type AddChannelRequest struct { @@ -171,7 +137,6 @@ func (r *AddChannelRequest) ToChannel() *model.Channel { Name: r.Name, Key: r.Key, BaseURL: r.BaseURL, - Other: r.Other, Models: slices.Clone(r.Models), ModelMapping: maps.Clone(r.ModelMapping), Config: r.Config, @@ -198,90 +163,67 @@ func AddChannel(c *gin.Context) { channel := AddChannelRequest{} err := c.ShouldBindJSON(&channel) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = model.BatchInsertChannels(channel.ToChannels()) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } func DeleteChannel(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) err := model.DeleteChannelByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) -} - -type UpdateChannelRequest struct { - AddChannelRequest - ID int `json:"id"` + middleware.SuccessResponse(c, nil) } -func (r *UpdateChannelRequest) ToChannel() *model.Channel { - c := r.AddChannelRequest.ToChannel() - c.ID = r.ID - return c +func DeleteChannels(c *gin.Context) { + ids := []int{} + err := c.ShouldBindJSON(&ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err = model.DeleteChannelsByIDs(ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) } func UpdateChannel(c *gin.Context) { - channel := UpdateChannelRequest{} - err := c.ShouldBindJSON(&channel) + idStr := c.Param("id") + if idStr == "" { + middleware.ErrorResponse(c, http.StatusOK, "id is required") + return + } + id, err := strconv.Atoi(idStr) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + channel := AddChannelRequest{} + err = c.ShouldBindJSON(&channel) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } ch := channel.ToChannel() + ch.ID = id err = model.UpdateChannel(ch) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": UpdateChannelRequest{ - ID: ch.ID, - AddChannelRequest: AddChannelRequest{ - Type: ch.Type, - Name: ch.Name, - Key: ch.Key, - BaseURL: ch.BaseURL, - Other: ch.Other, - Models: ch.Models, - ModelMapping: ch.ModelMapping, - Priority: ch.Priority, - Config: ch.Config, - }, - }, - }) + middleware.SuccessResponse(c, ch) } type UpdateChannelStatusRequest struct { @@ -293,22 +235,13 @@ func UpdateChannelStatus(c *gin.Context) { status := UpdateChannelStatusRequest{} err := c.ShouldBindJSON(&status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = model.UpdateChannelStatusByID(id, status.Status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } diff --git a/service/aiproxy/controller/group.go b/service/aiproxy/controller/group.go index 9a097da1775..b52e8a79e20 100644 --- a/service/aiproxy/controller/group.go +++ b/service/aiproxy/controller/group.go @@ -7,6 +7,7 @@ import ( json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" "github.com/gin-gonic/gin" @@ -28,20 +29,12 @@ func GetGroups(c *gin.Context) { order := c.DefaultQuery("order", "") groups, total, err := model.GetGroups(p*perPage, perPage, order, false) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "groups": groups, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "groups": groups, + "total": total, }) } @@ -62,44 +55,27 @@ func SearchGroups(c *gin.Context) { status, _ := strconv.Atoi(c.Query("status")) groups, total, err := model.SearchGroup(keyword, p*perPage, perPage, order, status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "groups": groups, - "total": total, - }, + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, gin.H{ + "groups": groups, + "total": total, }) } func GetGroup(c *gin.Context) { id := c.Param("id") if id == "" { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "group id is empty", - }) + middleware.ErrorResponse(c, http.StatusOK, "group id is empty") return } group, err := model.GetGroupByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": group, - }) + middleware.SuccessResponse(c, group) } func GetGroupDashboard(c *gin.Context) { @@ -110,18 +86,10 @@ func GetGroupDashboard(c *gin.Context) { dashboards, err := model.SearchLogsByDayAndModel(id, time.Unix(startOfDay, 0), time.Unix(endOfDay, 0)) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "failed to get statistics", - "data": nil, - }) + middleware.ErrorResponse(c, http.StatusOK, "failed to get statistics") return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": dashboards, - }) + middleware.SuccessResponse(c, dashboards) } type UpdateGroupQPMRequest struct { @@ -131,33 +99,21 @@ type UpdateGroupQPMRequest struct { func UpdateGroupQPM(c *gin.Context) { id := c.Param("id") if id == "" { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid parameter") return } req := UpdateGroupQPMRequest{} err := json.NewDecoder(c.Request.Body).Decode(&req) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid parameter") return } err = model.UpdateGroupQPM(id, req.QPM) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } type UpdateGroupStatusRequest struct { @@ -167,56 +123,50 @@ type UpdateGroupStatusRequest struct { func UpdateGroupStatus(c *gin.Context) { id := c.Param("id") if id == "" { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid parameter") return } req := UpdateGroupStatusRequest{} err := json.NewDecoder(c.Request.Body).Decode(&req) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid parameter") return } err = model.UpdateGroupStatus(id, req.Status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } func DeleteGroup(c *gin.Context) { id := c.Param("id") if id == "" { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid parameter") return } err := model.DeleteGroupByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) +} + +func DeleteGroups(c *gin.Context) { + ids := []string{} + err := c.ShouldBindJSON(&ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err = model.DeleteGroupsByIDs(ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) } type CreateGroupRequest struct { @@ -228,24 +178,15 @@ func CreateGroup(c *gin.Context) { var group CreateGroupRequest err := json.NewDecoder(c.Request.Body).Decode(&group) if err != nil || group.ID == "" { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid parameter") return } if err := model.CreateGroup(&model.Group{ ID: group.ID, QPM: group.QPM, }); err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } diff --git a/service/aiproxy/controller/log.go b/service/aiproxy/controller/log.go index 94279e66410..651aa610452 100644 --- a/service/aiproxy/controller/log.go +++ b/service/aiproxy/controller/log.go @@ -6,7 +6,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" ) @@ -41,23 +41,32 @@ func GetLogs(c *gin.Context) { content := c.Query("content") tokenID, _ := strconv.Atoi(c.Query("token_id")) order := c.Query("order") + requestID := c.Query("request_id") + mode, _ := strconv.Atoi(c.Query("mode")) logs, total, err := model.GetLogs( - startTimestampTime, endTimestampTime, - code, modelName, group, tokenID, tokenName, p*perPage, perPage, channel, endpoint, content, order) + startTimestampTime, + endTimestampTime, + code, + modelName, + group, + requestID, + tokenID, + tokenName, + p*perPage, + perPage, + channel, + endpoint, + content, + order, + mode, + ) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "logs": logs, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "logs": logs, + "total": total, }) } @@ -92,23 +101,32 @@ func GetGroupLogs(c *gin.Context) { content := c.Query("content") tokenID, _ := strconv.Atoi(c.Query("token_id")) order := c.Query("order") - logs, total, err := model.GetGroupLogs(group, - startTimestampTime, endTimestampTime, - code, modelName, tokenID, tokenName, p*perPage, perPage, channel, endpoint, content, order) + requestID := c.Query("request_id") + mode, _ := strconv.Atoi(c.Query("mode")) + logs, total, err := model.GetGroupLogs( + group, + startTimestampTime, + endTimestampTime, + code, + modelName, + requestID, + tokenID, + tokenName, + p*perPage, + perPage, + channel, + endpoint, + content, + order, + mode, + ) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "logs": logs, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "logs": logs, + "total": total, }) } @@ -140,21 +158,33 @@ func SearchLogs(c *gin.Context) { endTimestampTime = time.UnixMilli(endTimestamp) } order := c.Query("order") - logs, total, err := model.SearchLogs(keyword, p, perPage, code, endpoint, groupID, tokenID, tokenName, modelName, content, startTimestampTime, endTimestampTime, channel, order) + requestID := c.Query("request_id") + mode, _ := strconv.Atoi(c.Query("mode")) + logs, total, err := model.SearchLogs( + keyword, + p, + perPage, + code, + endpoint, + groupID, + requestID, + tokenID, + tokenName, + modelName, + content, + startTimestampTime, + endTimestampTime, + channel, + order, + mode, + ) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "logs": logs, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "logs": logs, + "total": total, }) } @@ -186,109 +216,48 @@ func SearchGroupLogs(c *gin.Context) { endTimestampTime = time.UnixMilli(endTimestamp) } order := c.Query("order") - logs, total, err := model.SearchGroupLogs(group, keyword, p, perPage, code, endpoint, tokenID, tokenName, modelName, content, startTimestampTime, endTimestampTime, channelID, order) + requestID := c.Query("request_id") + mode, _ := strconv.Atoi(c.Query("mode")) + logs, total, err := model.SearchGroupLogs( + group, + keyword, + p, + perPage, + code, + endpoint, + requestID, + tokenID, + tokenName, + modelName, + content, + startTimestampTime, + endTimestampTime, + channelID, + order, + mode, + ) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "logs": logs, - "total": total, - }, - }) -} - -func GetLogsStat(c *gin.Context) { - startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) - endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) - if endTimestamp < startTimestamp { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "end_timestamp must be greater than start_timestamp", - }) - return - } - tokenName := c.Query("token_name") - group := c.Query("group") - modelName := c.Query("model_name") - channel, _ := strconv.Atoi(c.Query("channel")) - endpoint := c.Query("endpoint") - var startTimestampTime time.Time - if startTimestamp != 0 { - startTimestampTime = time.UnixMilli(startTimestamp) - } - var endTimestampTime time.Time - if endTimestamp != 0 { - endTimestampTime = time.UnixMilli(endTimestamp) - } - quotaNum := model.SumUsedQuota(startTimestampTime, endTimestampTime, modelName, group, tokenName, channel, endpoint) - // tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "") - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "quota": quotaNum, - // "token": tokenNum, - }, - }) -} - -func GetLogsSelfStat(c *gin.Context) { - group := c.GetString(ctxkey.Group) - startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) - endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) - tokenName := c.Query("token_name") - modelName := c.Query("model_name") - channel, _ := strconv.Atoi(c.Query("channel")) - endpoint := c.Query("endpoint") - var startTimestampTime time.Time - if startTimestamp != 0 { - startTimestampTime = time.UnixMilli(startTimestamp) - } - var endTimestampTime time.Time - if endTimestamp != 0 { - endTimestampTime = time.UnixMilli(endTimestamp) - } - quotaNum := model.SumUsedQuota(startTimestampTime, endTimestampTime, modelName, group, tokenName, channel, endpoint) - // tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName) - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "quota": quotaNum, - // "token": tokenNum, - }, + middleware.SuccessResponse(c, gin.H{ + "logs": logs, + "total": total, }) } func DeleteHistoryLogs(c *gin.Context) { timestamp, _ := strconv.ParseInt(c.Query("timestamp"), 10, 64) if timestamp == 0 { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "timestamp is required", - }) + middleware.ErrorResponse(c, http.StatusOK, "timestamp is required") return } count, err := model.DeleteOldLog(time.UnixMilli(timestamp)) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": count, - }) + middleware.SuccessResponse(c, count) } func SearchConsumeError(c *gin.Context) { @@ -307,19 +276,14 @@ func SearchConsumeError(c *gin.Context) { perPage = 100 } order := c.Query("order") - logs, total, err := model.SearchConsumeError(keyword, group, tokenName, modelName, content, usedAmount, tokenID, page, perPage, order) + requestID := c.Query("request_id") + logs, total, err := model.SearchConsumeError(keyword, requestID, group, tokenName, modelName, content, usedAmount, tokenID, page, perPage, order) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "logs": logs, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "logs": logs, + "total": total, }) } diff --git a/service/aiproxy/controller/misc.go b/service/aiproxy/controller/misc.go index 06799d7cf1a..83f10290ff2 100644 --- a/service/aiproxy/controller/misc.go +++ b/service/aiproxy/controller/misc.go @@ -1,9 +1,8 @@ package controller import ( - "net/http" - "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/gin-gonic/gin" ) @@ -13,11 +12,7 @@ type StatusData struct { } func GetStatus(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": &StatusData{ - StartTime: common.StartTime, - }, + middleware.SuccessResponse(c, &StatusData{ + StartTime: common.StartTime, }) } diff --git a/service/aiproxy/controller/model.go b/service/aiproxy/controller/model.go index 8fd5b0f9966..2db6be6c1c1 100644 --- a/service/aiproxy/controller/model.go +++ b/service/aiproxy/controller/model.go @@ -4,19 +4,17 @@ import ( "fmt" "net/http" "slices" + "sort" "strconv" "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" - relay "github.com/labring/sealos/service/aiproxy/relay" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/apitype" "github.com/labring/sealos/service/aiproxy/relay/channeltype" - "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" - billingprice "github.com/labring/sealos/service/aiproxy/relay/price" + log "github.com/sirupsen/logrus" ) // https://platform.openai.com/docs/api-reference/models/list @@ -46,15 +44,31 @@ type OpenAIModels struct { Created int `json:"created"` } +type BuiltinModelConfig model.ModelConfig + +func (c *BuiltinModelConfig) MarshalJSON() ([]byte, error) { + type Alias BuiltinModelConfig + return json.Marshal(&struct { + *Alias + CreatedAt int64 `json:"created_at,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + }{ + Alias: (*Alias)(c), + }) +} + +func SortBuiltinModelConfigsFunc(i, j *BuiltinModelConfig) int { + return model.SortModelConfigsFunc((*model.ModelConfig)(i), (*model.ModelConfig)(j)) +} + var ( - models []OpenAIModels - modelsMap map[string]OpenAIModels - channelID2Models map[int][]string + builtinModels []*BuiltinModelConfig + builtinModelsMap map[string]*OpenAIModels + builtinChannelID2Models map[int][]*BuiltinModelConfig ) -func init() { - var permission []OpenAIModelPermission - permission = append(permission, OpenAIModelPermission{ +var permission = []OpenAIModelPermission{ + { ID: "modelperm-LwHkVFn8AcMItP432fKKDIKJ", Object: "model_permission", Created: 1626777600, @@ -67,295 +81,129 @@ func init() { Organization: "*", Group: nil, IsBlocking: false, - }) - // https://platform.openai.com/docs/models/model-endpoint-compatibility - for i := 0; i < apitype.Dummy; i++ { - if i == apitype.AIProxyLibrary { - continue - } - adaptor := relay.GetAdaptor(i) - adaptor.Init(&meta.Meta{ - ChannelType: i, - }) - channelName := adaptor.GetChannelName() - modelNames := adaptor.GetModelList() - for _, modelName := range modelNames { - models = append(models, OpenAIModels{ - ID: modelName, - Object: "model", - Created: 1626777600, - OwnedBy: channelName, - Permission: permission, - Root: modelName, - Parent: nil, - }) - } - } - for _, channelType := range openai.CompatibleChannels { - if channelType == channeltype.Azure { - continue - } - channelName, channelModelList := openai.GetCompatibleChannelMeta(channelType) - for _, modelName := range channelModelList { - models = append(models, OpenAIModels{ - ID: modelName, - Object: "model", - Created: 1626777600, - OwnedBy: channelName, - Permission: permission, - Root: modelName, - Parent: nil, - }) - } - } - modelsMap = make(map[string]OpenAIModels) - for _, model := range models { - modelsMap[model.ID] = model - } - channelID2Models = make(map[int][]string) - for i := 1; i < channeltype.Dummy; i++ { - adaptor := relay.GetAdaptor(channeltype.ToAPIType(i)) - meta := &meta.Meta{ - ChannelType: i, - } - adaptor.Init(meta) - channelID2Models[i] = adaptor.GetModelList() - } -} - -func BuiltinModels(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": channelID2Models, - }) + }, } -type modelPrice struct { - Prompt float64 `json:"prompt"` - Completion float64 `json:"completion"` - Unset bool `json:"unset,omitempty"` -} - -func ModelPrice(c *gin.Context) { - bill := make(map[string]*modelPrice) - modelPriceMap := billingprice.GetModelPriceMap() - completionPriceMap := billingprice.GetCompletionPriceMap() - for model, price := range modelPriceMap { - bill[model] = &modelPrice{ - Prompt: price, - Completion: price, - } - if completionPrice, ok := completionPriceMap[model]; ok { - bill[model].Completion = completionPrice - } - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": bill, - }) -} - -func EnabledType2Models(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": model.CacheGetType2Models(), - }) -} - -func EnabledType2ModelsAndPrice(c *gin.Context) { - type2Models := model.CacheGetType2Models() - result := make(map[int]map[string]*modelPrice) - - modelPriceMap := billingprice.GetModelPriceMap() - completionPriceMap := billingprice.GetCompletionPriceMap() - - for channelType, models := range type2Models { - m := make(map[string]*modelPrice) - result[channelType] = m - for _, modelName := range models { - if price, ok := modelPriceMap[modelName]; ok { - m[modelName] = &modelPrice{ - Prompt: price, - Completion: price, - } - if completionPrice, ok := completionPriceMap[modelName]; ok { - m[modelName].Completion = completionPrice - } - } else { - m[modelName] = &modelPrice{ - Unset: true, +func init() { + builtinChannelID2Models = make(map[int][]*BuiltinModelConfig) + builtinModelsMap = make(map[string]*OpenAIModels) + // https://platform.openai.com/docs/models/model-endpoint-compatibility + for i, adaptor := range channeltype.ChannelAdaptor { + modelNames := adaptor.GetModelList() + builtinChannelID2Models[i] = make([]*BuiltinModelConfig, len(modelNames)) + for idx, _model := range modelNames { + if _model.Owner == "" { + _model.Owner = model.ModelOwner(adaptor.GetChannelName()) + } + if v, ok := builtinModelsMap[_model.Model]; !ok { + builtinModelsMap[_model.Model] = &OpenAIModels{ + ID: _model.Model, + Object: "model", + Created: 1626777600, + OwnedBy: string(_model.Owner), + Permission: permission, + Root: _model.Model, + Parent: nil, } + builtinModels = append(builtinModels, (*BuiltinModelConfig)(_model)) + } else if v.OwnedBy != string(_model.Owner) { + log.Fatalf("model %s owner mismatch, expect %s, actual %s", _model.Model, string(_model.Owner), v.OwnedBy) } + builtinChannelID2Models[i][idx] = (*BuiltinModelConfig)(_model) } } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": result, - }) -} - -func ChannelDefaultModels(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": config.GetDefaultChannelModels(), - }) -} - -func ChannelDefaultModelsByType(c *gin.Context) { - channelType := c.Param("type") - if channelType == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "type is required", - }) - return - } - channelTypeInt, err := strconv.Atoi(channelType) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "invalid type", + for _, models := range builtinChannelID2Models { + sort.Slice(models, func(i, j int) bool { + return models[i].Model < models[j].Model }) - return + slices.SortStableFunc(models, SortBuiltinModelConfigsFunc) } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": config.GetDefaultChannelModels()[channelTypeInt], - }) + slices.SortStableFunc(builtinModels, SortBuiltinModelConfigsFunc) } -func ChannelDefaultModelMapping(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": config.GetDefaultChannelModelMapping(), - }) +func BuiltinModels(c *gin.Context) { + middleware.SuccessResponse(c, builtinModels) } -func ChannelDefaultModelMappingByType(c *gin.Context) { +func ChannelBuiltinModels(c *gin.Context) { + middleware.SuccessResponse(c, builtinChannelID2Models) +} + +func ChannelBuiltinModelsByType(c *gin.Context) { channelType := c.Param("type") if channelType == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "type is required", - }) + middleware.ErrorResponse(c, http.StatusOK, "type is required") return } channelTypeInt, err := strconv.Atoi(channelType) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "invalid type", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid type") return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": config.GetDefaultChannelModelMapping()[channelTypeInt], - }) + middleware.SuccessResponse(c, builtinChannelID2Models[channelTypeInt]) } func ChannelDefaultModelsAndMapping(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "models": config.GetDefaultChannelModels(), - "mapping": config.GetDefaultChannelModelMapping(), - }, + middleware.SuccessResponse(c, gin.H{ + "models": config.GetDefaultChannelModels(), + "mapping": config.GetDefaultChannelModelMapping(), }) } func ChannelDefaultModelsAndMappingByType(c *gin.Context) { channelType := c.Param("type") if channelType == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "type is required", - }) + middleware.ErrorResponse(c, http.StatusOK, "type is required") return } channelTypeInt, err := strconv.Atoi(channelType) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "invalid type", - }) + middleware.ErrorResponse(c, http.StatusOK, "invalid type") return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "models": config.GetDefaultChannelModels()[channelTypeInt], - "mapping": config.GetDefaultChannelModelMapping()[channelTypeInt], - }, + middleware.SuccessResponse(c, gin.H{ + "models": config.GetDefaultChannelModels()[channelTypeInt], + "mapping": config.GetDefaultChannelModelMapping()[channelTypeInt], }) } func EnabledModels(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": model.CacheGetAllModels(), - }) + middleware.SuccessResponse(c, model.CacheGetEnabledModelConfigs()) } -func EnabledModelsAndPrice(c *gin.Context) { - enabledModels := model.CacheGetAllModels() - result := make(map[string]*modelPrice) - - modelPriceMap := billingprice.GetModelPriceMap() - completionPriceMap := billingprice.GetCompletionPriceMap() +func ChannelEnabledModels(c *gin.Context) { + middleware.SuccessResponse(c, model.CacheGetEnabledChannelType2ModelConfigs()) +} - for _, modelName := range enabledModels { - if price, ok := modelPriceMap[modelName]; ok { - result[modelName] = &modelPrice{ - Prompt: price, - Completion: price, - } - if completionPrice, ok := completionPriceMap[modelName]; ok { - result[modelName].Completion = completionPrice - } - } else { - result[modelName] = &modelPrice{ - Unset: true, - } - } +func ChannelEnabledModelsByType(c *gin.Context) { + channelTypeStr := c.Param("type") + if channelTypeStr == "" { + middleware.ErrorResponse(c, http.StatusOK, "type is required") + return } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": result, - }) + channelTypeInt, err := strconv.Atoi(channelTypeStr) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, "invalid type") + return + } + middleware.SuccessResponse(c, model.CacheGetEnabledChannelType2ModelConfigs()[channelTypeInt]) } func ListModels(c *gin.Context) { - availableModels := c.GetStringSlice(ctxkey.AvailableModels) - availableOpenAIModels := make([]OpenAIModels, 0, len(availableModels)) - - for _, modelName := range availableModels { - if model, ok := modelsMap[modelName]; ok { - availableOpenAIModels = append(availableOpenAIModels, model) - continue + models := model.CacheGetEnabledModelConfigs() + + availableOpenAIModels := make([]*OpenAIModels, len(models)) + + for idx, model := range models { + availableOpenAIModels[idx] = &OpenAIModels{ + ID: model.Model, + Object: "model", + Created: 1626777600, + OwnedBy: string(model.Owner), + Root: model.Model, + Permission: permission, + Parent: nil, } - availableOpenAIModels = append(availableOpenAIModels, OpenAIModels{ - ID: modelName, - Object: "model", - Created: 1626777600, - OwnedBy: "custom", - Root: modelName, - Parent: nil, - }) } c.JSON(http.StatusOK, gin.H{ @@ -365,12 +213,14 @@ func ListModels(c *gin.Context) { } func RetrieveModel(c *gin.Context) { - modelID := c.Param("model") - model, ok := modelsMap[modelID] - if !ok || !slices.Contains(c.GetStringSlice(ctxkey.AvailableModels), modelID) { + modelName := c.Param("model") + enabledModels := model.GetEnabledModel2Channels() + model, ok := model.CacheGetModelConfig(modelName) + + if _, exist := enabledModels[modelName]; !exist || !ok { c.JSON(200, gin.H{ - "error": relaymodel.Error{ - Message: fmt.Sprintf("the model '%s' does not exist", modelID), + "error": &relaymodel.Error{ + Message: fmt.Sprintf("the model '%s' does not exist", modelName), Type: "invalid_request_error", Param: "model", Code: "model_not_found", @@ -378,5 +228,14 @@ func RetrieveModel(c *gin.Context) { }) return } - c.JSON(200, model) + + c.JSON(200, &OpenAIModels{ + ID: model.Model, + Object: "model", + Created: 1626777600, + OwnedBy: string(model.Owner), + Root: model.Model, + Permission: permission, + Parent: nil, + }) } diff --git a/service/aiproxy/controller/modelconfig.go b/service/aiproxy/controller/modelconfig.go new file mode 100644 index 00000000000..94501cd4806 --- /dev/null +++ b/service/aiproxy/controller/modelconfig.go @@ -0,0 +1,151 @@ +package controller + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/model" +) + +func GetModelConfigs(c *gin.Context) { + p, _ := strconv.Atoi(c.Query("p")) + p-- + if p < 0 { + p = 0 + } + perPage, _ := strconv.Atoi(c.Query("per_page")) + if perPage <= 0 { + perPage = 10 + } else if perPage > 100 { + perPage = 100 + } + _model := c.Query("model") + configs, total, err := model.GetModelConfigs(p*perPage, perPage, _model) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, gin.H{ + "configs": configs, + "total": total, + }) +} + +func GetAllModelConfigs(c *gin.Context) { + configs, err := model.GetAllModelConfigs() + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, configs) +} + +type GetModelConfigsByModelsContainsRequest struct { + Models []string `json:"models"` +} + +func GetModelConfigsByModelsContains(c *gin.Context) { + request := GetModelConfigsByModelsContainsRequest{} + err := c.ShouldBindJSON(&request) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + configs, err := model.GetModelConfigsByModels(request.Models) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, configs) +} + +func SearchModelConfigs(c *gin.Context) { + keyword := c.Query("keyword") + p, _ := strconv.Atoi(c.Query("p")) + p-- + if p < 0 { + p = 0 + } + perPage, _ := strconv.Atoi(c.Query("per_page")) + if perPage <= 0 { + perPage = 10 + } else if perPage > 100 { + perPage = 100 + } + _model := c.Query("model") + owner := c.Query("owner") + configs, total, err := model.SearchModelConfigs(keyword, p*perPage, perPage, _model, model.ModelOwner(owner)) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, gin.H{ + "configs": configs, + "total": total, + }) +} + +func SaveModelConfigs(c *gin.Context) { + var configs []*model.ModelConfig + if err := c.ShouldBindJSON(&configs); err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err := model.SaveModelConfigs(configs) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) +} + +func SaveModelConfig(c *gin.Context) { + var config model.ModelConfig + if err := c.ShouldBindJSON(&config); err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err := model.SaveModelConfig(&config) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) +} + +func DeleteModelConfig(c *gin.Context) { + _model := c.Param("model") + err := model.DeleteModelConfig(_model) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) +} + +func DeleteModelConfigs(c *gin.Context) { + models := []string{} + err := c.ShouldBindJSON(&models) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err = model.DeleteModelConfigsByModels(models) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) +} + +func GetModelConfig(c *gin.Context) { + _model := c.Param("model") + config, err := model.GetModelConfig(_model) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, config) +} diff --git a/service/aiproxy/controller/option.go b/service/aiproxy/controller/option.go index dd3c273ccec..6197331bb49 100644 --- a/service/aiproxy/controller/option.go +++ b/service/aiproxy/controller/option.go @@ -5,70 +5,51 @@ import ( json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/config" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" "github.com/gin-gonic/gin" ) func GetOptions(c *gin.Context) { - options := make(map[string]string) - config.OptionMapRWMutex.RLock() - for k, v := range config.OptionMap { - options[k] = v + dbOptions, err := model.GetAllOption() + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + options := make(map[string]string, len(dbOptions)) + for _, option := range dbOptions { + options[option.Key] = option.Value } - config.OptionMapRWMutex.RUnlock() - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": options, - }) + middleware.SuccessResponse(c, options) } func UpdateOption(c *gin.Context) { var option model.Option err := json.NewDecoder(c.Request.Body).Decode(&option) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = model.UpdateOption(option.Key, option.Value) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } func UpdateOptions(c *gin.Context) { var options map[string]string err := json.NewDecoder(c.Request.Body).Decode(&options) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "invalid parameter", - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = model.UpdateOptions(options) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } diff --git a/service/aiproxy/controller/relay.go b/service/aiproxy/controller/relay.go index 892861f1062..1d30cef78af 100644 --- a/service/aiproxy/controller/relay.go +++ b/service/aiproxy/controller/relay.go @@ -2,112 +2,99 @@ package controller import ( "bytes" - "context" - "fmt" "io" "net/http" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/middleware" dbmodel "github.com/labring/sealos/service/aiproxy/model" - "github.com/labring/sealos/service/aiproxy/monitor" "github.com/labring/sealos/service/aiproxy/relay/controller" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) // https://platform.openai.com/docs/api-reference/chat -func relayHelper(c *gin.Context, relayMode int) *model.ErrorWithStatusCode { - var err *model.ErrorWithStatusCode - switch relayMode { +func relayHelper(meta *meta.Meta, c *gin.Context) *model.ErrorWithStatusCode { + log := middleware.GetLogger(c) + middleware.SetLogFieldsFromMeta(meta, log.Data) + switch meta.Mode { case relaymode.ImagesGenerations: - err = controller.RelayImageHelper(c, relayMode) + return controller.RelayImageHelper(meta, c) case relaymode.AudioSpeech: - fallthrough + return controller.RelayTTSHelper(meta, c) case relaymode.AudioTranslation: - fallthrough + return controller.RelaySTTHelper(meta, c) case relaymode.AudioTranscription: - err = controller.RelayAudioHelper(c, relayMode) + return controller.RelaySTTHelper(meta, c) case relaymode.Rerank: - err = controller.RerankHelper(c) + return controller.RerankHelper(meta, c) default: - err = controller.RelayTextHelper(c) + return controller.RelayTextHelper(meta, c) } - return err } func Relay(c *gin.Context) { - ctx := c.Request.Context() - relayMode := relaymode.GetByPath(c.Request.URL.Path) + log := middleware.GetLogger(c) if config.DebugEnabled { - requestBody, _ := common.GetRequestBody(c) - logger.Debugf(ctx, "request body: %s", requestBody) + requestBody, _ := common.GetRequestBody(c.Request) + log.Debugf("request body: %s", requestBody) } - channelID := c.GetInt(ctxkey.ChannelID) - bizErr := relayHelper(c, relayMode) + meta := middleware.NewMetaByContext(c) + bizErr := relayHelper(meta, c) if bizErr == nil { - monitor.Emit(channelID, true) return } - lastFailedChannelID := channelID - group := c.GetString(ctxkey.Group) - originalModel := c.GetString(ctxkey.OriginalModel) - go processChannelRelayError(ctx, group, channelID, bizErr) + lastFailedChannelID := meta.Channel.ID requestID := c.GetString(string(helper.RequestIDKey)) retryTimes := config.GetRetryTimes() if !shouldRetry(c, bizErr.StatusCode) { - logger.Errorf(ctx, "relay error happen, status code is %d, won't retry in this case", bizErr.StatusCode) retryTimes = 0 } for i := retryTimes; i > 0; i-- { - channel, err := dbmodel.CacheGetRandomSatisfiedChannel(originalModel) + channel, err := dbmodel.CacheGetRandomSatisfiedChannel(meta.OriginModelName) if err != nil { - logger.Errorf(ctx, "get random satisfied channel failed: %+v", err) + log.Errorf("get random satisfied channel failed: %+v", err) break } - logger.Infof(ctx, "using channel #%d to retry (remain times %d)", channel.ID, i) + log.Infof("using channel #%d to retry (remain times %d)", channel.ID, i) if channel.ID == lastFailedChannelID { continue } - middleware.SetupContextForSelectedChannel(c, channel, originalModel) - requestBody, err := common.GetRequestBody(c) + requestBody, err := common.GetRequestBody(c.Request) if err != nil { - logger.Errorf(ctx, "GetRequestBody failed: %+v", err) + log.Errorf("GetRequestBody failed: %+v", err) break } c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) - bizErr = relayHelper(c, relayMode) + meta.Reset(channel) + bizErr = relayHelper(meta, c) if bizErr == nil { return } - channelID := c.GetInt(ctxkey.ChannelID) - lastFailedChannelID = channelID - // BUG: bizErr is in race condition - go processChannelRelayError(ctx, group, channelID, bizErr) + lastFailedChannelID = channel.ID } if bizErr != nil { + message := bizErr.Message if bizErr.StatusCode == http.StatusTooManyRequests { - bizErr.Error.Message = "The upstream load of the current group is saturated, please try again later" + message = "The upstream load of the current group is saturated, please try again later" } - - // BUG: bizErr is in race condition - bizErr.Error.Message = helper.MessageWithRequestID(bizErr.Error.Message, requestID) c.JSON(bizErr.StatusCode, gin.H{ - "error": bizErr.Error, + "error": &model.Error{ + Message: helper.MessageWithRequestID(message, requestID), + Code: bizErr.Code, + Param: bizErr.Param, + Type: bizErr.Type, + }, }) } } -func shouldRetry(c *gin.Context, statusCode int) bool { - if _, ok := c.Get(ctxkey.SpecificChannelID); ok { - return false - } +func shouldRetry(_ *gin.Context, statusCode int) bool { if statusCode == http.StatusTooManyRequests { return true } @@ -123,36 +110,13 @@ func shouldRetry(c *gin.Context, statusCode int) bool { return true } -func processChannelRelayError(ctx context.Context, group string, channelID int, err *model.ErrorWithStatusCode) { - logger.Errorf(ctx, "relay error (channel id %d, group: %s): %s", channelID, group, err) - // https://platform.openai.com/docs/guides/error-codes/api-errors - if monitor.ShouldDisableChannel(&err.Error, err.StatusCode) { - _ = dbmodel.DisableChannelByID(channelID) - } else { - monitor.Emit(channelID, false) - } -} - func RelayNotImplemented(c *gin.Context) { - err := model.Error{ - Message: "API not implemented", - Type: "aiproxy_error", - Param: "", - Code: "api_not_implemented", - } c.JSON(http.StatusNotImplemented, gin.H{ - "error": err, - }) -} - -func RelayNotFound(c *gin.Context) { - err := model.Error{ - Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path), - Type: "invalid_request_error", - Param: "", - Code: "", - } - c.JSON(http.StatusNotFound, gin.H{ - "error": err, + "error": &model.Error{ + Message: "API not implemented", + Type: middleware.ErrorTypeAIPROXY, + Param: "", + Code: "api_not_implemented", + }, }) } diff --git a/service/aiproxy/controller/token.go b/service/aiproxy/controller/token.go index 1abfb77b172..090800004ce 100644 --- a/service/aiproxy/controller/token.go +++ b/service/aiproxy/controller/token.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common/network" "github.com/labring/sealos/service/aiproxy/common/random" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" ) @@ -30,19 +31,12 @@ func GetTokens(c *gin.Context) { status, _ := strconv.Atoi(c.Query("status")) tokens, total, err := model.GetTokens(p*perPage, perPage, order, group, status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "tokens": tokens, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "tokens": tokens, + "total": total, }) } @@ -63,19 +57,12 @@ func GetGroupTokens(c *gin.Context) { status, _ := strconv.Atoi(c.Query("status")) tokens, total, err := model.GetGroupTokens(group, p*perPage, perPage, order, status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "tokens": tokens, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "tokens": tokens, + "total": total, }) } @@ -99,19 +86,12 @@ func SearchTokens(c *gin.Context) { group := c.Query("group") tokens, total, err := model.SearchTokens(keyword, p*perPage, perPage, order, status, name, key, group) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "tokens": tokens, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "tokens": tokens, + "total": total, }) } @@ -135,69 +115,42 @@ func SearchGroupTokens(c *gin.Context) { status, _ := strconv.Atoi(c.Query("status")) tokens, total, err := model.SearchGroupTokens(group, keyword, p*perPage, perPage, order, status, name, key) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "tokens": tokens, - "total": total, - }, + middleware.SuccessResponse(c, gin.H{ + "tokens": tokens, + "total": total, }) } func GetToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } token, err := model.GetTokenByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": token, - }) + middleware.SuccessResponse(c, token) } func GetGroupToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } group := c.Param("group") token, err := model.GetGroupTokenByID(group, id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": token, - }) + middleware.SuccessResponse(c, token) } func validateToken(token AddTokenRequest) error { @@ -229,18 +182,12 @@ func AddToken(c *gin.Context) { token := AddTokenRequest{} err := c.ShouldBindJSON(&token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = validateToken(token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "parameter error: " + err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, "parameter error: "+err.Error()) return } @@ -262,98 +209,92 @@ func AddToken(c *gin.Context) { } err = model.InsertToken(cleanToken, c.Query("auto_create_group") == "true") if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": cleanToken, - }) + middleware.SuccessResponse(c, cleanToken) } func DeleteToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = model.DeleteTokenByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) +} + +func DeleteTokens(c *gin.Context) { + ids := []int{} + err := c.ShouldBindJSON(&ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err = model.DeleteTokensByIDs(ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) } func DeleteGroupToken(c *gin.Context) { group := c.Param("group") id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - err = model.DeleteTokenByIDAndGroupID(id, group) + err = model.DeleteGroupTokenByID(group, id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) +} + +func DeleteGroupTokens(c *gin.Context) { + group := c.Param("group") + ids := []int{} + err := c.ShouldBindJSON(&ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + err = model.DeleteGroupTokensByIDs(group, ids) + if err != nil { + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return + } + middleware.SuccessResponse(c, nil) } func UpdateToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } token := AddTokenRequest{} err = c.ShouldBindJSON(&token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = validateToken(token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "parameter error: " + err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, "parameter error: "+err.Error()) return } cleanToken, err := model.GetTokenByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } expiredAt := time.Time{} @@ -367,52 +308,33 @@ func UpdateToken(c *gin.Context) { cleanToken.Subnet = token.Subnet err = model.UpdateToken(cleanToken) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": cleanToken, - }) + middleware.SuccessResponse(c, cleanToken) } func UpdateGroupToken(c *gin.Context) { group := c.Param("group") id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } token := AddTokenRequest{} err = c.ShouldBindJSON(&token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } err = validateToken(token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "parameter error: " + err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, "parameter error: "+err.Error()) return } cleanToken, err := model.GetGroupTokenByID(group, id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } expiredAt := time.Time{} @@ -426,17 +348,10 @@ func UpdateGroupToken(c *gin.Context) { cleanToken.Subnet = token.Subnet err = model.UpdateToken(cleanToken) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": cleanToken, - }) + middleware.SuccessResponse(c, cleanToken) } type UpdateTokenStatusRequest struct { @@ -446,57 +361,40 @@ type UpdateTokenStatusRequest struct { func UpdateTokenStatus(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } token := UpdateTokenStatusRequest{} err = c.ShouldBindJSON(&token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } cleanToken, err := model.GetTokenByID(id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } if token.Status == model.TokenStatusEnabled { if cleanToken.Status == model.TokenStatusExpired && !cleanToken.ExpiredAt.IsZero() && cleanToken.ExpiredAt.Before(time.Now()) { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期", - }) + middleware.ErrorResponse(c, http.StatusOK, "token expired, please update token expired time or set to never expire") + return + } + if cleanToken.Status == model.TokenStatusExhausted && cleanToken.Quota > 0 && cleanToken.UsedAmount >= cleanToken.Quota { + middleware.ErrorResponse(c, http.StatusOK, "token quota exhausted, please update token quota or set to unlimited quota") return } if cleanToken.Status == model.TokenStatusExhausted && cleanToken.Quota > 0 && cleanToken.UsedAmount >= cleanToken.Quota { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度", - }) + middleware.ErrorResponse(c, http.StatusOK, "token quota exhausted, please update token quota or set to unlimited quota") return } } err = model.UpdateTokenStatus(id, token.Status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } type UpdateGroupTokenStatusRequest struct { @@ -507,57 +405,40 @@ func UpdateGroupTokenStatus(c *gin.Context) { group := c.Param("group") id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } token := UpdateTokenStatusRequest{} err = c.ShouldBindJSON(&token) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } cleanToken, err := model.GetGroupTokenByID(group, id) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } if token.Status == model.TokenStatusEnabled { if cleanToken.Status == model.TokenStatusExpired && !cleanToken.ExpiredAt.IsZero() && cleanToken.ExpiredAt.Before(time.Now()) { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期", - }) + middleware.ErrorResponse(c, http.StatusOK, "token expired, please update token expired time or set to never expire") + return + } + if cleanToken.Status == model.TokenStatusExhausted && cleanToken.Quota > 0 && cleanToken.UsedAmount >= cleanToken.Quota { + middleware.ErrorResponse(c, http.StatusOK, "token quota exhausted, please update token quota or set to unlimited quota") return } if cleanToken.Status == model.TokenStatusExhausted && cleanToken.Quota > 0 && cleanToken.UsedAmount >= cleanToken.Quota { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度", - }) + middleware.ErrorResponse(c, http.StatusOK, "token quota exhausted, please update token quota or set to unlimited quota") return } } err = model.UpdateGroupTokenStatus(group, id, token.Status) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } type UpdateTokenNameRequest struct { @@ -567,58 +448,40 @@ type UpdateTokenNameRequest struct { func UpdateTokenName(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } name := UpdateTokenNameRequest{} err = c.ShouldBindJSON(&name) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } err = model.UpdateTokenName(id, name.Name) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } func UpdateGroupTokenName(c *gin.Context) { group := c.Param("group") id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } name := UpdateTokenNameRequest{} err = c.ShouldBindJSON(&name) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } err = model.UpdateGroupTokenName(group, id, name.Name) if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) + middleware.ErrorResponse(c, http.StatusOK, err.Error()) + return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) + middleware.SuccessResponse(c, nil) } diff --git a/service/aiproxy/go.mod b/service/aiproxy/go.mod index c44cbcabc63..8e382e3c96d 100644 --- a/service/aiproxy/go.mod +++ b/service/aiproxy/go.mod @@ -5,44 +5,47 @@ go 1.22.7 replace github.com/labring/sealos/service/aiproxy => ../aiproxy require ( - cloud.google.com/go/iam v1.2.2 - github.com/aws/aws-sdk-go-v2 v1.32.4 - github.com/aws/aws-sdk-go-v2/credentials v1.17.44 - github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.20.0 + cloud.google.com/go/iam v1.3.0 + github.com/aws/aws-sdk-go-v2 v1.32.6 + github.com/aws/aws-sdk-go-v2/credentials v1.17.47 + github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.23.0 github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/gzip v1.0.1 github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/jinzhu/copier v0.4.0 github.com/joho/godotenv v1.5.1 github.com/json-iterator/go v1.1.12 + github.com/maruel/natural v1.1.1 + github.com/mattn/go-isatty v0.0.20 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/pkoukk/tiktoken-go v0.1.7 github.com/redis/go-redis/v9 v9.7.0 github.com/shopspring/decimal v1.4.0 + github.com/sirupsen/logrus v1.9.3 github.com/smartystreets/goconvey v1.8.1 github.com/stretchr/testify v1.9.0 - golang.org/x/image v0.22.0 - google.golang.org/api v0.205.0 + golang.org/x/image v0.23.0 + google.golang.org/api v0.210.0 gorm.io/driver/mysql v1.5.7 - gorm.io/driver/postgres v1.5.9 + gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 ) require ( - cloud.google.com/go/auth v0.10.2 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect + cloud.google.com/go/auth v0.12.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect - github.com/aws/smithy-go v1.22.0 // indirect - github.com/bytedance/sonic v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect + github.com/aws/smithy-go v1.22.1 // indirect + github.com/bytedance/sonic v1.12.5 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -52,17 +55,17 @@ require ( github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.3 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect @@ -77,7 +80,6 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect @@ -94,21 +96,21 @@ require ( go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/net v0.31.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect + golang.org/x/net v0.32.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect - google.golang.org/grpc v1.68.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.61.0 // indirect + modernc.org/libc v1.61.4 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.33.1 // indirect + modernc.org/sqlite v1.34.2 // indirect ) diff --git a/service/aiproxy/go.sum b/service/aiproxy/go.sum index e48ba2706ce..59895f5af14 100644 --- a/service/aiproxy/go.sum +++ b/service/aiproxy/go.sum @@ -1,35 +1,35 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo= -cloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= -cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= -cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/auth v0.12.0 h1:ARAD8r0lkiHw2go7kEnmviF6TOYhzLM+yDGcDt9mP68= +cloud.google.com/go/auth v0.12.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= -cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iam v1.3.0 h1:4Wo2qTaGKFtajbLpF6I4mywg900u3TLlHDb6mriLDPU= +cloud.google.com/go/iam v1.3.0/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= -github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.20.0 h1:c/2Lv0Nq/I+UeWKqUKR/LS9rO8McuXc5CzIfK2aBlhg= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.20.0/go.mod h1:Kh/nzScDldU7Ti7MyFMCA+0Po+LZ4iNjWwl7H1DWYtU= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.23.0 h1:mfV5tcLXeRLbiyI4EHoHWH1sIU7JvbfXVvymUCIgZEo= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.23.0/go.mod h1:YSSgYnasDKm5OjU3bOPkaz+2PFO6WjEQGIA6KQNsR3Q= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= -github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w= +github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -58,8 +58,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= -github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= @@ -83,21 +83,19 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -132,6 +130,8 @@ github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPq github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -162,6 +162,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -190,6 +192,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= @@ -224,13 +228,13 @@ golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= -golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= -golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -242,27 +246,28 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -270,27 +275,27 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.205.0 h1:LFaxkAIpDb/GsrWV20dMMo5MR0h8UARTbn24LmD+0Pg= -google.golang.org/api v0.205.0/go.mod h1:NrK1EMqO8Xk6l6QwRAmrXXg2v6dzukhlOyvkYtnvUuc= +google.golang.org/api v0.210.0 h1:HMNffZ57OoZCRYSbdWVRoqOa8V8NIHLL0CzdBPLztWk= +google.golang.org/api v0.210.0/go.mod h1:B9XDZGnx2NtyjzVkOVTGrFSAVZgPcbedzKg/gTLwqBs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f h1:M65LEviCfuZTfrfzwwEoxVtgvfkFkBUbFnRbxCXuXhU= -google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -310,23 +315,23 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= -gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= -modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= -modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8= +modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.23.1 h1:N49a7JiWGWV7lkPE4yYcvjkBGZQi93/JabRYjdWmJXc= +modernc.org/ccgo/v4 v4.23.1/go.mod h1:JoIUegEIfutvoWV/BBfDFpPpfR2nc3U0jKucGcbmwDU= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= -modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= +modernc.org/libc v1.61.4 h1:wVyqEx6tlltte9lPTjq0kDAdtdM9c4JH8rU6M1ZVawA= +modernc.org/libc v1.61.4/go.mod h1:VfXVuM/Shh5XsMNrh3C6OkfL78G3loa4ZC/Ljv9k7xc= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -335,8 +340,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= -modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/sqlite v1.34.2 h1:J9n76TPsfYYkFkZ9Uy1QphILYifiVEwwOT7yP5b++2Y= +modernc.org/sqlite v1.34.2/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/service/aiproxy/main.go b/service/aiproxy/main.go index fb4d5e0ae63..38a7f392766 100644 --- a/service/aiproxy/main.go +++ b/service/aiproxy/main.go @@ -2,10 +2,14 @@ package main import ( "context" + "fmt" + stdlog "log" "net/http" "os" "os/signal" + "runtime" "strconv" + "sync" "syscall" "time" @@ -15,110 +19,169 @@ import ( "github.com/labring/sealos/service/aiproxy/common/balance" "github.com/labring/sealos/service/aiproxy/common/client" "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/controller" "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" relaycontroller "github.com/labring/sealos/service/aiproxy/relay/controller" "github.com/labring/sealos/service/aiproxy/router" + log "github.com/sirupsen/logrus" ) -func main() { +func initializeServices() error { + setLog(log.StandardLogger()) + common.Init() - logger.SetupLogger() + if err := initializeBalance(); err != nil { + return err + } + + if err := initializeDatabases(); err != nil { + return err + } + + if err := initializeCaches(); err != nil { + return err + } + + client.Init() + return nil +} + +func initializeBalance() error { sealosJwtKey := os.Getenv("SEALOS_JWT_KEY") if sealosJwtKey == "" { - logger.SysLog("SEALOS_JWT_KEY is not set, balance will not be enabled") - } else { - logger.SysLog("SEALOS_JWT_KEY is set, balance will be enabled") - err := balance.InitSealos(sealosJwtKey, os.Getenv("SEALOS_ACCOUNT_URL")) - if err != nil { - logger.FatalLog("failed to initialize sealos balance: " + err.Error()) - } + log.Info("SEALOS_JWT_KEY is not set, balance will not be enabled") + return nil } - if os.Getenv("GIN_MODE") != gin.DebugMode { + log.Info("SEALOS_JWT_KEY is set, balance will be enabled") + return balance.InitSealos(sealosJwtKey, os.Getenv("SEALOS_ACCOUNT_URL")) +} + +var logCallerIgnoreFuncs = map[string]struct{}{ + "github.com/labring/sealos/service/aiproxy/middleware.logColor": {}, +} + +func setLog(l *log.Logger) { + gin.ForceConsoleColor() + if config.DebugEnabled { + l.SetLevel(log.DebugLevel) + l.SetReportCaller(true) + gin.SetMode(gin.DebugMode) + } else { + l.SetLevel(log.InfoLevel) + l.SetReportCaller(false) gin.SetMode(gin.ReleaseMode) } - if config.DebugEnabled { - logger.SysLog("running in debug mode") + l.SetOutput(os.Stdout) + stdlog.SetOutput(l.Writer()) + + log.SetFormatter(&log.TextFormatter{ + ForceColors: true, + DisableColors: false, + ForceQuote: config.DebugEnabled, + DisableQuote: !config.DebugEnabled, + DisableSorting: false, + FullTimestamp: true, + TimestampFormat: time.DateTime, + QuoteEmptyFields: true, + CallerPrettyfier: func(f *runtime.Frame) (function string, file string) { + if _, ok := logCallerIgnoreFuncs[f.Function]; ok { + return "", "" + } + return f.Function, fmt.Sprintf("%s:%d", f.File, f.Line) + }, + }) + + if common.NeedColor() { + gin.ForceConsoleColor() } +} - // Initialize SQL Database +func initializeDatabases() error { model.InitDB() model.InitLogDB() + return common.InitRedisClient() +} - defer func() { - err := model.CloseDB() - if err != nil { - logger.FatalLog("failed to close database: " + err.Error()) - } - }() - - // Initialize Redis - err := common.InitRedisClient() - if err != nil { - logger.FatalLog("failed to initialize Redis: " + err.Error()) - } - - // Initialize options - model.InitOptionMap() - model.InitChannelCache() - go model.SyncOptions(time.Second * 5) - go model.SyncChannelCache(time.Second * 5) - if os.Getenv("CHANNEL_TEST_FREQUENCY") != "" { - frequency, err := strconv.Atoi(os.Getenv("CHANNEL_TEST_FREQUENCY")) - if err != nil { - logger.FatalLog("failed to parse CHANNEL_TEST_FREQUENCY: " + err.Error()) - } - go controller.AutomaticallyTestChannels(frequency) +func initializeCaches() error { + if err := model.InitOptionMap(); err != nil { + return err } - if config.EnableMetric { - logger.SysLog("metric enabled, will disable channel if too much request failed") + if err := model.InitModelConfigCache(); err != nil { + return err } - client.Init() + return model.InitChannelCache() +} - // Initialize HTTP server +func startSyncServices(ctx context.Context, wg *sync.WaitGroup) { + wg.Add(3) + go model.SyncOptions(ctx, wg, time.Second*5) + go model.SyncChannelCache(ctx, wg, time.Second*5) + go model.SyncModelConfigCache(ctx, wg, time.Second*5) +} + +func setupHTTPServer() (*http.Server, *gin.Engine) { server := gin.New() - server.Use(gin.Recovery()) - server.Use(middleware.RequestID) - middleware.SetUpLogger(server) + w := log.StandardLogger().Writer() + server. + Use(middleware.NewLog(log.StandardLogger())). + Use(gin.RecoveryWithWriter(w)). + Use(middleware.RequestID) router.SetRouter(server) + port := os.Getenv("PORT") if port == "" { port = strconv.Itoa(*common.Port) } - // Create HTTP server - srv := &http.Server{ + return &http.Server{ Addr: ":" + port, ReadHeaderTimeout: 10 * time.Second, Handler: server, + }, server +} + +func main() { + if err := initializeServices(); err != nil { + log.Fatal("failed to initialize services: " + err.Error()) } - // Graceful shutdown setup + defer func() { + if err := model.CloseDB(); err != nil { + log.Fatal("failed to close database: " + err.Error()) + } + }() + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + var wg sync.WaitGroup + startSyncServices(ctx, &wg) + + srv, _ := setupHTTPServer() + go func() { - logger.SysLogf("server started on http://localhost:%s", port) + log.Infof("server started on http://localhost:%s", srv.Addr[1:]) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.FatalLog("failed to start HTTP server: " + err.Error()) + log.Fatal("failed to start HTTP server: " + err.Error()) } }() - // Wait for interrupt signal to gracefully shutdown the server - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt, syscall.SIGTERM) - <-quit - logger.SysLog("shutting down server...") + <-ctx.Done() + log.Info("shutting down server...") + log.Info("max wait time: 120s") - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + shutdownCtx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() - if err := srv.Shutdown(ctx); err != nil { - logger.SysError("server forced to shutdown: " + err.Error()) + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Error("server forced to shutdown: " + err.Error()) } relaycontroller.ConsumeWaitGroup.Wait() + wg.Wait() - logger.SysLog("server exiting") + log.Info("server exiting") } diff --git a/service/aiproxy/middleware/auth.go b/service/aiproxy/middleware/auth.go index a278eddc902..a8408f1abcd 100644 --- a/service/aiproxy/middleware/auth.go +++ b/service/aiproxy/middleware/auth.go @@ -3,7 +3,6 @@ package middleware import ( "fmt" "net/http" - "slices" "strings" "time" @@ -12,15 +11,34 @@ import ( "github.com/labring/sealos/service/aiproxy/common/ctxkey" "github.com/labring/sealos/service/aiproxy/common/network" "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/sirupsen/logrus" ) +type APIResponse struct { + Data any `json:"data,omitempty"` + Message string `json:"message,omitempty"` + Success bool `json:"success"` +} + +func SuccessResponse(c *gin.Context, data any) { + c.JSON(http.StatusOK, &APIResponse{ + Success: true, + Data: data, + }) +} + +func ErrorResponse(c *gin.Context, code int, message string) { + c.JSON(code, &APIResponse{ + Success: false, + Message: message, + }) +} + func AdminAuth(c *gin.Context) { accessToken := c.Request.Header.Get("Authorization") if config.AdminKey != "" && (accessToken == "" || strings.TrimPrefix(accessToken, "Bearer ") != config.AdminKey) { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "unauthorized, no access token provided", - }) + ErrorResponse(c, http.StatusUnauthorized, "unauthorized, no access token provided") c.Abort() return } @@ -28,6 +46,7 @@ func AdminAuth(c *gin.Context) { } func TokenAuth(c *gin.Context) { + log := GetLogger(c) ctx := c.Request.Context() key := c.Request.Header.Get("Authorization") key = strings.TrimPrefix( @@ -41,8 +60,12 @@ func TokenAuth(c *gin.Context) { abortWithMessage(c, http.StatusUnauthorized, err.Error()) return } + SetLogTokenFields(log.Data, token) if token.Subnet != "" { - if !network.IsIPInSubnets(ctx, c.ClientIP(), token.Subnet) { + if ok, err := network.IsIPInSubnets(c.ClientIP(), token.Subnet); err != nil { + abortWithMessage(c, http.StatusInternalServerError, err.Error()) + return + } else if !ok { abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("token (%s[%d]) can only be used in the specified subnet: %s, current ip: %s", token.Name, @@ -59,39 +82,13 @@ func TokenAuth(c *gin.Context) { abortWithMessage(c, http.StatusInternalServerError, err.Error()) return } - requestModel, err := getRequestModel(c) - if err != nil && shouldCheckModel(c) { - abortWithMessage(c, http.StatusBadRequest, err.Error()) - return - } - c.Set(ctxkey.RequestModel, requestModel) + SetLogGroupFields(log.Data, group) if len(token.Models) == 0 { - token.Models = model.CacheGetAllModels() - if requestModel != "" && len(token.Models) == 0 { - abortWithMessage(c, - http.StatusForbidden, - fmt.Sprintf("token (%s[%d]) has no permission to use any model", - token.Name, token.ID, - ), - ) - return - } - } - c.Set(ctxkey.AvailableModels, []string(token.Models)) - if requestModel != "" && !slices.Contains(token.Models, requestModel) { - abortWithMessage(c, - http.StatusForbidden, - fmt.Sprintf("token (%s[%d]) has no permission to use model: %s", - token.Name, token.ID, requestModel, - ), - ) - return + token.Models = model.CacheGetEnabledModels() } - if group.QPM <= 0 { group.QPM = config.GetDefaultGroupQPM() } - if group.QPM > 0 { ok := ForceRateLimit(ctx, "group_qpm:"+group.ID, int(group.QPM), time.Minute) if !ok { @@ -102,36 +99,73 @@ func TokenAuth(c *gin.Context) { } } - c.Set(ctxkey.Group, token.Group) - c.Set(ctxkey.GroupQPM, group.QPM) - c.Set(ctxkey.TokenID, token.ID) - c.Set(ctxkey.TokenName, token.Name) - c.Set(ctxkey.TokenUsedAmount, token.UsedAmount) - c.Set(ctxkey.TokenQuota, token.Quota) - // if len(parts) > 1 { - // c.Set(ctxkey.SpecificChannelId, parts[1]) - // } - - // set channel id for proxy relay - if channelID := c.Param("channelid"); channelID != "" { - c.Set(ctxkey.SpecificChannelID, channelID) - } + c.Set(ctxkey.Group, group) + c.Set(ctxkey.Token, token) c.Next() } -func shouldCheckModel(c *gin.Context) bool { - if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") { - return true +func SetLogFieldsFromMeta(m *meta.Meta, fields logrus.Fields) { + SetLogRequestIDField(fields, m.RequestID) + + SetLogModeField(fields, m.Mode) + SetLogModelFields(fields, m.OriginModelName) + SetLogActualModelFields(fields, m.ActualModelName) + + if m.IsChannelTest { + SetLogIsChannelTestField(fields, true) + } + + SetLogGroupFields(fields, m.Group) + SetLogTokenFields(fields, m.Token) + SetLogChannelFields(fields, m.Channel) +} + +func SetLogModeField(fields logrus.Fields, mode int) { + fields["mode"] = mode +} + +func SetLogIsChannelTestField(fields logrus.Fields, isChannelTest bool) { + fields["test"] = isChannelTest +} + +func SetLogActualModelFields(fields logrus.Fields, actualModel string) { + fields["actmodel"] = actualModel +} + +func SetLogModelFields(fields logrus.Fields, model string) { + fields["model"] = model +} + +func SetLogChannelFields(fields logrus.Fields, channel *meta.ChannelMeta) { + if channel != nil { + fields["chid"] = channel.ID + fields["chname"] = channel.Name + fields["chtype"] = channel.Type } - if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { - return true +} + +func SetLogRequestIDField(fields logrus.Fields, requestID string) { + fields["reqid"] = requestID +} + +func SetLogGroupFields(fields logrus.Fields, group *model.GroupCache) { + if group != nil { + fields["gid"] = group.ID } - if strings.HasPrefix(c.Request.URL.Path, "/v1/images") { - return true +} + +func SetLogTokenFields(fields logrus.Fields, token *model.TokenCache) { + if token != nil { + fields["tid"] = token.ID + fields["tname"] = token.Name + fields["key"] = maskTokenKey(token.Key) } - if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") { - return true +} + +func maskTokenKey(key string) string { + if len(key) <= 8 { + return "*****" } - return false + return key[:4] + "*****" + key[len(key)-4:] } diff --git a/service/aiproxy/middleware/distributor.go b/service/aiproxy/middleware/distributor.go index 826f1c34568..c0501d35be4 100644 --- a/service/aiproxy/middleware/distributor.go +++ b/service/aiproxy/middleware/distributor.go @@ -1,15 +1,18 @@ package middleware import ( + "context" + "fmt" "net/http" "slices" - "strconv" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/ctxkey" + "github.com/labring/sealos/service/aiproxy/common/helper" "github.com/labring/sealos/service/aiproxy/model" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) type ModelRequest struct { @@ -21,70 +24,57 @@ func Distribute(c *gin.Context) { abortWithMessage(c, http.StatusServiceUnavailable, "service is under maintenance") return } - requestModel := c.GetString(ctxkey.RequestModel) + + log := GetLogger(c) + + requestModel, err := getRequestModel(c) + if err != nil { + abortWithMessage(c, http.StatusBadRequest, err.Error()) + return + } if requestModel == "" { abortWithMessage(c, http.StatusBadRequest, "no model provided") return } - var channel *model.Channel - channelID, ok := c.Get(ctxkey.SpecificChannelID) - if ok { - id, err := strconv.Atoi(channelID.(string)) - if err != nil { - abortWithMessage(c, http.StatusBadRequest, "invalid channel ID") - return - } - channel, ok = model.CacheGetChannelByID(id) - if !ok { - abortWithMessage(c, http.StatusBadRequest, "invalid channel ID") - return - } - if !slices.Contains(channel.Models, requestModel) { - abortWithMessage(c, http.StatusServiceUnavailable, channel.Name+" does not support "+requestModel) - return - } - } else { - var err error - channel, err = model.CacheGetRandomSatisfiedChannel(requestModel) - if err != nil { - message := requestModel + " is not available" - abortWithMessage(c, http.StatusServiceUnavailable, message) - return - } + + SetLogModelFields(log.Data, requestModel) + + token := c.MustGet(ctxkey.Token).(*model.TokenCache) + if len(token.Models) == 0 || !slices.Contains(token.Models, requestModel) { + abortWithMessage(c, + http.StatusForbidden, + fmt.Sprintf("token (%s[%d]) has no permission to use model: %s", + token.Name, token.ID, requestModel, + ), + ) + return + } + channel, err := model.CacheGetRandomSatisfiedChannel(requestModel) + if err != nil { + abortWithMessage(c, http.StatusServiceUnavailable, requestModel+" is not available") + return } - SetupContextForSelectedChannel(c, channel, requestModel) + + c.Set(string(ctxkey.OriginalModel), requestModel) + ctx := context.WithValue(c.Request.Context(), ctxkey.OriginalModel, requestModel) + c.Request = c.Request.WithContext(ctx) + c.Set(ctxkey.Channel, channel) + c.Next() } -func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) { - c.Set(ctxkey.Channel, channel.Type) - c.Set(ctxkey.ChannelID, channel.ID) - c.Set(ctxkey.APIKey, channel.Key) - c.Set(ctxkey.ChannelName, channel.Name) - c.Set(ctxkey.ModelMapping, channel.ModelMapping) - c.Set(ctxkey.OriginalModel, modelName) // for retry - c.Set(ctxkey.BaseURL, channel.BaseURL) - cfg := channel.Config - // this is for backward compatibility - if channel.Other != "" { - switch channel.Type { - case channeltype.Azure: - if cfg.APIVersion == "" { - cfg.APIVersion = channel.Other - } - case channeltype.Gemini: - if cfg.APIVersion == "" { - cfg.APIVersion = channel.Other - } - case channeltype.AIProxyLibrary: - if cfg.LibraryID == "" { - cfg.LibraryID = channel.Other - } - case channeltype.Ali: - if cfg.Plugin == "" { - cfg.Plugin = channel.Other - } - } - } - c.Set(ctxkey.Config, cfg) +func NewMetaByContext(c *gin.Context) *meta.Meta { + channel := c.MustGet(ctxkey.Channel).(*model.Channel) + originalModel := c.MustGet(string(ctxkey.OriginalModel)).(string) + requestID := c.GetString(string(helper.RequestIDKey)) + group := c.MustGet(ctxkey.Group).(*model.GroupCache) + token := c.MustGet(ctxkey.Token).(*model.TokenCache) + return meta.NewMeta( + channel, + relaymode.GetByPath(c.Request.URL.Path), + originalModel, + meta.WithRequestID(requestID), + meta.WithGroup(group), + meta.WithToken(token), + ) } diff --git a/service/aiproxy/middleware/log.go b/service/aiproxy/middleware/log.go new file mode 100644 index 00000000000..2af86d17be1 --- /dev/null +++ b/service/aiproxy/middleware/log.go @@ -0,0 +1,110 @@ +package middleware + +import ( + "fmt" + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +var fieldsPool = sync.Pool{ + New: func() interface{} { + return make(logrus.Fields, 6) + }, +} + +func NewLog(l *logrus.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + fields := fieldsPool.Get().(logrus.Fields) + defer func() { + clear(fields) + fieldsPool.Put(fields) + }() + + entry := &logrus.Entry{ + Logger: l, + Data: fields, + } + c.Set("log", entry) + + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + c.Next() + + param := gin.LogFormatterParams{ + Request: c.Request, + Keys: c.Keys, + } + + // Stop timer + param.Latency = time.Since(start) + + param.ClientIP = c.ClientIP() + param.Method = c.Request.Method + param.StatusCode = c.Writer.Status() + param.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + + param.BodySize = c.Writer.Size() + + if raw != "" { + path = path + "?" + raw + } + + param.Path = path + + logColor(entry, param) + } +} + +func logColor(log *logrus.Entry, p gin.LogFormatterParams) { + str := formatter(p) + code := p.StatusCode + switch { + case code >= http.StatusBadRequest && code < http.StatusInternalServerError: + log.Error(str) + default: + log.Info(str) + } +} + +func formatter(param gin.LogFormatterParams) string { + var statusColor, methodColor, resetColor string + if param.IsOutputColor() { + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() + } + + if param.Latency > time.Minute { + param.Latency = param.Latency.Truncate(time.Second) + } + return fmt.Sprintf("[GIN] |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s", + statusColor, param.StatusCode, resetColor, + param.Latency, + param.ClientIP, + methodColor, param.Method, resetColor, + param.Path, + param.ErrorMessage, + ) +} + +func GetLogger(c *gin.Context) *logrus.Entry { + if log, ok := c.Get("log"); ok { + return log.(*logrus.Entry) + } + entry := NewLogger() + c.Set("log", entry) + return entry +} + +func NewLogger() *logrus.Entry { + return &logrus.Entry{ + Logger: logrus.StandardLogger(), + Data: fieldsPool.Get().(logrus.Fields), + } +} diff --git a/service/aiproxy/middleware/logger.go b/service/aiproxy/middleware/logger.go deleted file mode 100644 index 75c3fa66dbc..00000000000 --- a/service/aiproxy/middleware/logger.go +++ /dev/null @@ -1,26 +0,0 @@ -package middleware - -import ( - "fmt" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/helper" -) - -func SetUpLogger(server *gin.Engine) { - server.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - var requestID string - if param.Keys != nil { - requestID = param.Keys[string(helper.RequestIDKey)].(string) - } - return fmt.Sprintf("[GIN] %s | %s | %3d | %13v | %15s | %7s %s\n", - param.TimeStamp.Format("2006/01/02 - 15:04:05"), - requestID, - param.StatusCode, - param.Latency, - param.ClientIP, - param.Method, - param.Path, - ) - })) -} diff --git a/service/aiproxy/middleware/rate-limit.go b/service/aiproxy/middleware/rate-limit.go index 2783ab458b1..12ebfafc4b5 100644 --- a/service/aiproxy/middleware/rate-limit.go +++ b/service/aiproxy/middleware/rate-limit.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" ) var inMemoryRateLimiter common.InMemoryRateLimiter @@ -74,7 +74,7 @@ func ForceRateLimit(ctx context.Context, key string, maxRequestNum int, duration if err == nil { return ok } - logger.Error(ctx, "rate limit error: "+err.Error()) + log.Error("rate limit error: " + err.Error()) } return MemoryRateLimit(ctx, key, maxRequestNum, duration) } diff --git a/service/aiproxy/middleware/recover.go b/service/aiproxy/middleware/recover.go deleted file mode 100644 index d76c1792ccb..00000000000 --- a/service/aiproxy/middleware/recover.go +++ /dev/null @@ -1,32 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http" - "runtime/debug" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/logger" -) - -func RelayPanicRecover(c *gin.Context) { - defer func() { - if err := recover(); err != nil { - ctx := c.Request.Context() - logger.Errorf(ctx, "panic detected: %v", err) - logger.Errorf(ctx, "stacktrace from panic: %s", debug.Stack()) - logger.Errorf(ctx, "request: %s %s", c.Request.Method, c.Request.URL.Path) - body, _ := common.GetRequestBody(c) - logger.Errorf(ctx, "request body: %s", body) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": gin.H{ - "message": fmt.Sprintf("Panic detected, error: %v.", err), - "type": "aiproxy_panic", - }, - }) - c.Abort() - } - }() - c.Next() -} diff --git a/service/aiproxy/middleware/request-id.go b/service/aiproxy/middleware/request-id.go index aabca3a04e3..159ebb220c8 100644 --- a/service/aiproxy/middleware/request-id.go +++ b/service/aiproxy/middleware/request-id.go @@ -1,8 +1,6 @@ package middleware import ( - "context" - "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common/helper" ) @@ -10,8 +8,8 @@ import ( func RequestID(c *gin.Context) { id := helper.GenRequestID() c.Set(string(helper.RequestIDKey), id) - ctx := context.WithValue(c.Request.Context(), helper.RequestIDKey, id) - c.Request = c.Request.WithContext(ctx) c.Header(string(helper.RequestIDKey), id) + log := GetLogger(c) + SetLogRequestIDField(log.Data, id) c.Next() } diff --git a/service/aiproxy/middleware/utils.go b/service/aiproxy/middleware/utils.go index 91eedb46c03..e80a6177867 100644 --- a/service/aiproxy/middleware/utils.go +++ b/service/aiproxy/middleware/utils.go @@ -7,32 +7,35 @@ import ( "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/relay/model" +) + +const ( + ErrorTypeAIPROXY = "aiproxy_error" ) func abortWithMessage(c *gin.Context, statusCode int, message string) { + GetLogger(c).Error(message) c.JSON(statusCode, gin.H{ - "error": gin.H{ - "message": helper.MessageWithRequestID(message, c.GetString(string(helper.RequestIDKey))), - "type": "aiproxy_error", + "error": &model.Error{ + Message: helper.MessageWithRequestID(message, c.GetString(string(helper.RequestIDKey))), + Type: ErrorTypeAIPROXY, }, }) c.Abort() - logger.Error(c.Request.Context(), message) } func getRequestModel(c *gin.Context) (string, error) { path := c.Request.URL.Path switch { - case strings.HasPrefix(path, "/v1/moderations"): - return "text-moderation-stable", nil - case strings.HasPrefix(path, "/v1/images/generations"): - return "dall-e-2", nil case strings.HasPrefix(path, "/v1/audio/transcriptions"), strings.HasPrefix(path, "/v1/audio/translations"): return c.Request.FormValue("model"), nil + case strings.HasPrefix(path, "/v1/engines") && strings.HasSuffix(path, "/embeddings"): + // /engines/:model/embeddings + return c.Param("model"), nil default: var modelRequest ModelRequest - err := common.UnmarshalBodyReusable(c, &modelRequest) + err := common.UnmarshalBodyReusable(c.Request, &modelRequest) if err != nil { return "", fmt.Errorf("get request model failed: %w", err) } diff --git a/service/aiproxy/model/cache.go b/service/aiproxy/model/cache.go index be58a19cb20..858c02f9101 100644 --- a/service/aiproxy/model/cache.go +++ b/service/aiproxy/model/cache.go @@ -6,17 +6,19 @@ import ( "errors" "fmt" "math/rand/v2" + "slices" "sort" "sync" "time" json "github.com/json-iterator/go" + "github.com/maruel/natural" "github.com/redis/go-redis/v9" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" ) const ( @@ -67,6 +69,7 @@ func (t *Token) ToTokenCache() *TokenCache { return &TokenCache{ ID: t.ID, Group: t.GroupID, + Key: t.Key, Name: t.Name.String(), Models: t.Models, Subnet: t.Subnet, @@ -84,6 +87,7 @@ func CacheDeleteToken(key string) error { return common.RedisDel(fmt.Sprintf(TokenCacheKey, key)) } +//nolint:gosec func CacheSetToken(token *Token) error { if !common.RedisEnabled { return nil @@ -112,8 +116,8 @@ func CacheGetTokenByKey(key string) (*TokenCache, error) { if err == nil && tokenCache.ID != 0 { tokenCache.Key = key return tokenCache, nil - } else if err != nil && err != redis.Nil { - logger.SysLogf("get token (%s) from redis error: %s", key, err.Error()) + } else if err != nil && !errors.Is(err, redis.Nil) { + log.Errorf("get token (%s) from redis error: %s", key, err.Error()) } token, err := GetTokenByKey(key) @@ -122,7 +126,7 @@ func CacheGetTokenByKey(key string) (*TokenCache, error) { } if err := CacheSetToken(token); err != nil { - logger.SysError("redis set token error: " + err.Error()) + log.Error("redis set token error: " + err.Error()) } return token.ToTokenCache(), nil @@ -226,6 +230,7 @@ func CacheUpdateGroupStatus(id string, status int) error { return updateGroupStatusScript.Run(context.Background(), common.RDB, []string{fmt.Sprintf(GroupCacheKey, id)}, status).Err() } +//nolint:gosec func CacheSetGroup(group *Group) error { if !common.RedisEnabled { return nil @@ -255,7 +260,7 @@ func CacheGetGroup(id string) (*GroupCache, error) { groupCache.ID = id return groupCache, nil } else if err != nil && !errors.Is(err, redis.Nil) { - logger.SysLogf("get group (%s) from redis error: %s", id, err.Error()) + log.Errorf("get group (%s) from redis error: %s", id, err.Error()) } group, err := GetGroupByID(id) @@ -264,105 +269,333 @@ func CacheGetGroup(id string) (*GroupCache, error) { } if err := CacheSetGroup(group); err != nil { - logger.SysError("redis set group error: " + err.Error()) + log.Error("redis set group error: " + err.Error()) } return group.ToGroupCache(), nil } var ( - model2channels map[string][]*Channel - allModels []string - type2Models map[int][]string - channelID2channel map[int]*Channel - channelSyncLock sync.RWMutex + enabledChannels []*Channel + allChannels []*Channel + enabledModel2channels map[string][]*Channel + enabledModels []string + enabledModelConfigs []*ModelConfig + enabledChannelType2ModelConfigs map[int][]*ModelConfig + enabledChannelID2channel map[int]*Channel + allChannelID2channel map[int]*Channel + channelSyncLock sync.RWMutex ) -func CacheGetAllModels() []string { +func CacheGetAllChannels() []*Channel { + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + return allChannels +} + +func CacheGetAllChannelByID(id int) (*Channel, bool) { + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + channel, ok := allChannelID2channel[id] + return channel, ok +} + +// GetEnabledModel2Channels returns a map of model name to enabled channels +func GetEnabledModel2Channels() map[string][]*Channel { + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + return enabledModel2channels +} + +// CacheGetEnabledModels returns a list of enabled model names +func CacheGetEnabledModels() []string { + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + return enabledModels +} + +// CacheGetEnabledChannelType2ModelConfigs returns a map of channel type to enabled model configs +func CacheGetEnabledChannelType2ModelConfigs() map[int][]*ModelConfig { + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + return enabledChannelType2ModelConfigs +} + +// CacheGetEnabledModelConfigs returns a list of enabled model configs +func CacheGetEnabledModelConfigs() []*ModelConfig { + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + return enabledModelConfigs +} + +func CacheGetEnabledChannels() []*Channel { channelSyncLock.RLock() defer channelSyncLock.RUnlock() - return allModels + return enabledChannels } -func CacheGetType2Models() map[int][]string { +func CacheGetEnabledChannelByID(id int) (*Channel, bool) { channelSyncLock.RLock() defer channelSyncLock.RUnlock() - return type2Models + channel, ok := enabledChannelID2channel[id] + return channel, ok +} + +// InitChannelCache initializes the channel cache from database +func InitChannelCache() error { + // Load enabled newEnabledChannels from database + newEnabledChannels, err := LoadEnabledChannels() + if err != nil { + return err + } + + // Load all channels from database + newAllChannels, err := LoadChannels() + if err != nil { + return err + } + + // Build channel ID to channel map + newEnabledChannelID2channel := buildChannelIDMap(newEnabledChannels) + + // Build all channel ID to channel map + newAllChannelID2channel := buildChannelIDMap(newAllChannels) + + // Build model to channels map + newEnabledModel2channels := buildModelToChannelsMap(newEnabledChannels) + + // Sort channels by priority + sortChannelsByPriority(newEnabledModel2channels) + + // Build channel type to model configs map + newEnabledChannelType2ModelConfigs := buildChannelTypeToModelConfigsMap(newEnabledChannels) + + // Build enabled models and configs lists + newEnabledModels, newEnabledModelConfigs := buildEnabledModelsAndConfigs(newEnabledChannelType2ModelConfigs) + + // Update global cache atomically + updateGlobalCache( + newEnabledChannels, + newAllChannels, + newEnabledModel2channels, + newEnabledModels, + newEnabledModelConfigs, + newEnabledChannelID2channel, + newEnabledChannelType2ModelConfigs, + newAllChannelID2channel, + ) + + return nil } -func CacheGetModelsByType(channelType int) []string { - return CacheGetType2Models()[channelType] +func LoadEnabledChannels() ([]*Channel, error) { + var channels []*Channel + err := DB.Where("status = ?", ChannelStatusEnabled).Find(&channels).Error + if err != nil { + return nil, err + } + + for _, channel := range channels { + initializeChannelModels(channel) + initializeChannelModelMapping(channel) + } + + return channels, nil } -func InitChannelCache() { - newChannelID2channel := make(map[int]*Channel) +func LoadChannels() ([]*Channel, error) { var channels []*Channel - DB.Where("status = ?", ChannelStatusEnabled).Find(&channels) + err := DB.Find(&channels).Error + if err != nil { + return nil, err + } + for _, channel := range channels { - if len(channel.Models) == 0 { - channel.Models = config.GetDefaultChannelModels()[channel.Type] - } - if len(channel.ModelMapping) == 0 { - channel.ModelMapping = config.GetDefaultChannelModelMapping()[channel.Type] - } - newChannelID2channel[channel.ID] = channel + initializeChannelModels(channel) + initializeChannelModelMapping(channel) + } + + return channels, nil +} + +func LoadChannelByID(id int) (*Channel, error) { + var channel Channel + err := DB.First(&channel, id).Error + if err != nil { + return nil, err + } + + initializeChannelModels(&channel) + initializeChannelModelMapping(&channel) + + return &channel, nil +} + +func initializeChannelModels(channel *Channel) { + if len(channel.Models) == 0 { + channel.Models = config.GetDefaultChannelModels()[channel.Type] + return + } + + findedModels, missingModels, err := CheckModelConfig(channel.Models) + if err != nil { + return + } + + if len(missingModels) > 0 { + slices.Sort(missingModels) + log.Errorf("model config not found: %v", missingModels) } - newModel2channels := make(map[string][]*Channel) + slices.Sort(findedModels) + channel.Models = findedModels +} + +func initializeChannelModelMapping(channel *Channel) { + if len(channel.ModelMapping) == 0 { + channel.ModelMapping = config.GetDefaultChannelModelMapping()[channel.Type] + } +} + +func buildChannelIDMap(channels []*Channel) map[int]*Channel { + channelMap := make(map[int]*Channel) + for _, channel := range channels { + channelMap[channel.ID] = channel + } + return channelMap +} + +func buildModelToChannelsMap(channels []*Channel) map[string][]*Channel { + modelMap := make(map[string][]*Channel) for _, channel := range channels { for _, model := range channel.Models { - newModel2channels[model] = append(newModel2channels[model], channel) + modelMap[model] = append(modelMap[model], channel) } } + return modelMap +} - // sort by priority - for _, channels := range newModel2channels { +func sortChannelsByPriority(modelMap map[string][]*Channel) { + for _, channels := range modelMap { sort.Slice(channels, func(i, j int) bool { return channels[i].Priority > channels[j].Priority }) } +} - models := make([]string, 0, len(newModel2channels)) - for model := range newModel2channels { - models = append(models, model) - } +func buildChannelTypeToModelConfigsMap(channels []*Channel) map[int][]*ModelConfig { + typeMap := make(map[int][]*ModelConfig) - newType2ModelsMap := make(map[int]map[string]struct{}) for _, channel := range channels { - newType2ModelsMap[channel.Type] = make(map[string]struct{}) + if _, ok := typeMap[channel.Type]; !ok { + typeMap[channel.Type] = make([]*ModelConfig, 0, len(channel.Models)) + } + configs := typeMap[channel.Type] + for _, model := range channel.Models { - newType2ModelsMap[channel.Type][model] = struct{}{} + if config, ok := CacheGetModelConfig(model); ok { + configs = append(configs, config) + } + } + typeMap[channel.Type] = configs + } + + for key, configs := range typeMap { + slices.SortStableFunc(configs, SortModelConfigsFunc) + typeMap[key] = slices.CompactFunc(configs, func(e1, e2 *ModelConfig) bool { + return e1.Model == e2.Model + }) + } + return typeMap +} + +func buildEnabledModelsAndConfigs(typeMap map[int][]*ModelConfig) ([]string, []*ModelConfig) { + models := make([]string, 0) + configs := make([]*ModelConfig, 0) + appended := make(map[string]struct{}) + + for _, modelConfigs := range typeMap { + for _, config := range modelConfigs { + if _, ok := appended[config.Model]; ok { + continue + } + models = append(models, config.Model) + configs = append(configs, config) + appended[config.Model] = struct{}{} + } + } + + slices.Sort(models) + slices.SortStableFunc(configs, SortModelConfigsFunc) + + return models, configs +} + +func SortModelConfigsFunc(i, j *ModelConfig) int { + if i.Owner != j.Owner { + if natural.Less(string(i.Owner), string(j.Owner)) { + return -1 } + return 1 } - newType2Models := make(map[int][]string) - for k, v := range newType2ModelsMap { - newType2Models[k] = make([]string, 0, len(v)) - for model := range v { - newType2Models[k] = append(newType2Models[k], model) + if i.Type != j.Type { + if i.Type < j.Type { + return -1 } + return 1 + } + if i.Model == j.Model { + return 0 } + if natural.Less(i.Model, j.Model) { + return -1 + } + return 1 +} +func updateGlobalCache( + newEnabledChannels []*Channel, + newAllChannels []*Channel, + newEnabledModel2channels map[string][]*Channel, + newEnabledModels []string, + newEnabledModelConfigs []*ModelConfig, + newEnabledChannelID2channel map[int]*Channel, + newEnabledChannelType2ModelConfigs map[int][]*ModelConfig, + newAllChannelID2channel map[int]*Channel, +) { channelSyncLock.Lock() - model2channels = newModel2channels - allModels = models - type2Models = newType2Models - channelID2channel = newChannelID2channel - channelSyncLock.Unlock() - logger.SysDebug("channels synced from database") + defer channelSyncLock.Unlock() + enabledChannels = newEnabledChannels + allChannels = newAllChannels + enabledModel2channels = newEnabledModel2channels + enabledModels = newEnabledModels + enabledModelConfigs = newEnabledModelConfigs + enabledChannelID2channel = newEnabledChannelID2channel + enabledChannelType2ModelConfigs = newEnabledChannelType2ModelConfigs + allChannelID2channel = newAllChannelID2channel } -func SyncChannelCache(frequency time.Duration) { +func SyncChannelCache(ctx context.Context, wg *sync.WaitGroup, frequency time.Duration) { + defer wg.Done() + ticker := time.NewTicker(frequency) defer ticker.Stop() - for range ticker.C { - logger.SysDebug("syncing channels from database") - InitChannelCache() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := InitChannelCache() + if err != nil { + log.Error("failed to sync channels: " + err.Error()) + continue + } + } } } +//nolint:gosec func CacheGetRandomSatisfiedChannel(model string) (*Channel, error) { - channelSyncLock.RLock() - channels := model2channels[model] - channelSyncLock.RUnlock() + channels := GetEnabledModel2Channels()[model] if len(channels) == 0 { return nil, errors.New("model not found") } @@ -391,9 +624,64 @@ func CacheGetRandomSatisfiedChannel(model string) (*Channel, error) { return channels[rand.IntN(len(channels))], nil } -func CacheGetChannelByID(id int) (*Channel, bool) { - channelSyncLock.RLock() - channel, ok := channelID2channel[id] - channelSyncLock.RUnlock() - return channel, ok +var ( + modelConfigSyncLock sync.RWMutex + modelConfigMap map[string]*ModelConfig +) + +func InitModelConfigCache() error { + modelConfigs, err := GetAllModelConfigs() + if err != nil { + return err + } + newModelConfigMap := make(map[string]*ModelConfig) + for _, modelConfig := range modelConfigs { + newModelConfigMap[modelConfig.Model] = modelConfig + } + + modelConfigSyncLock.Lock() + modelConfigMap = newModelConfigMap + modelConfigSyncLock.Unlock() + return nil +} + +func SyncModelConfigCache(ctx context.Context, wg *sync.WaitGroup, frequency time.Duration) { + defer wg.Done() + + ticker := time.NewTicker(frequency) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := InitModelConfigCache() + if err != nil { + log.Error("failed to sync model configs: " + err.Error()) + } + } + } +} + +func CacheGetModelConfig(model string) (*ModelConfig, bool) { + modelConfigSyncLock.RLock() + defer modelConfigSyncLock.RUnlock() + modelConfig, ok := modelConfigMap[model] + return modelConfig, ok +} + +func CacheCheckModelConfig(models []string) ([]string, []string) { + if len(models) == 0 { + return models, nil + } + founded := make([]string, 0) + missing := make([]string, 0) + for _, model := range models { + if _, ok := modelConfigMap[model]; ok { + founded = append(founded, model) + } else { + missing = append(missing, model) + } + } + return founded, missing } diff --git a/service/aiproxy/model/channel.go b/service/aiproxy/model/channel.go index 28dd6a133f5..de5ab2d3501 100644 --- a/service/aiproxy/model/channel.go +++ b/service/aiproxy/model/channel.go @@ -9,7 +9,6 @@ import ( "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -26,89 +25,117 @@ const ( ) type Channel struct { - CreatedAt time.Time `gorm:"index" json:"created_at"` + CreatedAt time.Time `gorm:"index" json:"created_at"` AccessedAt time.Time `json:"accessed_at"` - TestAt time.Time `json:"test_at"` + LastTestErrorAt time.Time `json:"last_test_error_at"` + ChannelTests []*ChannelTest `gorm:"foreignKey:ChannelID;references:ID" json:"channel_tests"` BalanceUpdatedAt time.Time `json:"balance_updated_at"` - ModelMapping map[string]string `gorm:"serializer:fastjson;type:text" json:"model_mapping"` - Config ChannelConfig `gorm:"serializer:fastjson;type:text" json:"config"` - Other string `json:"other"` - Key string `gorm:"type:text;index" json:"key"` - Name string `gorm:"index" json:"name"` - BaseURL string `gorm:"index" json:"base_url"` - Models []string `gorm:"serializer:fastjson;type:text" json:"models"` + ModelMapping map[string]string `gorm:"serializer:fastjson;type:text" json:"model_mapping"` + Config ChannelConfig `gorm:"serializer:fastjson;type:text" json:"config"` + Key string `gorm:"type:text;index" json:"key"` + Name string `gorm:"index" json:"name"` + BaseURL string `gorm:"index" json:"base_url"` + Models []string `gorm:"serializer:fastjson;type:text" json:"models"` Balance float64 `json:"balance"` - ResponseDuration int64 `gorm:"index" json:"response_duration"` - ID int `gorm:"primaryKey" json:"id"` - UsedAmount float64 `gorm:"index" json:"used_amount"` - RequestCount int `gorm:"index" json:"request_count"` - Status int `gorm:"default:1;index" json:"status"` - Type int `gorm:"default:0;index" json:"type"` + ID int `gorm:"primaryKey" json:"id"` + UsedAmount float64 `gorm:"index" json:"used_amount"` + RequestCount int `gorm:"index" json:"request_count"` + Status int `gorm:"default:1;index" json:"status"` + Type int `gorm:"default:0;index" json:"type"` Priority int32 `json:"priority"` } +func (c *Channel) BeforeDelete(tx *gorm.DB) (err error) { + return tx.Model(&ChannelTest{}).Where("channel_id = ?", c.ID).Delete(&ChannelTest{}).Error +} + +// check model config exist +func (c *Channel) BeforeSave(tx *gorm.DB) (err error) { + if len(c.Models) == 0 { + return nil + } + _, missingModels, err := checkModelConfig(tx, c.Models) + if err != nil { + return err + } + if len(missingModels) > 0 { + return fmt.Errorf("model config not found: %v", missingModels) + } + return nil +} + +func CheckModelConfig(models []string) ([]string, []string, error) { + return checkModelConfig(DB, models) +} + +func checkModelConfig(tx *gorm.DB, models []string) ([]string, []string, error) { + if len(models) == 0 { + return models, nil, nil + } + + where := tx.Model(&ModelConfig{}).Where("model IN ?", models) + var count int64 + if err := where.Count(&count).Error; err != nil { + return nil, nil, err + } + if count == 0 { + return nil, models, nil + } + if count == int64(len(models)) { + return models, nil, nil + } + + var foundModels []string + if err := where.Pluck("model", &foundModels).Error; err != nil { + return nil, nil, err + } + if len(foundModels) == len(models) { + return models, nil, nil + } + foundModelsMap := make(map[string]struct{}, len(foundModels)) + for _, model := range foundModels { + foundModelsMap[model] = struct{}{} + } + if len(models)-len(foundModels) > 0 { + missingModels := make([]string, 0, len(models)-len(foundModels)) + for _, model := range models { + if _, exists := foundModelsMap[model]; !exists { + missingModels = append(missingModels, model) + } + } + return foundModels, missingModels, nil + } + return foundModels, nil, nil +} + func (c *Channel) MarshalJSON() ([]byte, error) { type Alias Channel return json.Marshal(&struct { *Alias CreatedAt int64 `json:"created_at"` AccessedAt int64 `json:"accessed_at"` - TestAt int64 `json:"test_at"` BalanceUpdatedAt int64 `json:"balance_updated_at"` + LastTestErrorAt int64 `json:"last_test_error_at"` }{ Alias: (*Alias)(c), CreatedAt: c.CreatedAt.UnixMilli(), AccessedAt: c.AccessedAt.UnixMilli(), - TestAt: c.TestAt.UnixMilli(), BalanceUpdatedAt: c.BalanceUpdatedAt.UnixMilli(), + LastTestErrorAt: c.LastTestErrorAt.UnixMilli(), }) } //nolint:goconst func getChannelOrder(order string) string { - switch order { - case "name": - return "name asc" - case "name-desc": - return "name desc" - case "type": - return "type asc" - case "type-desc": - return "type desc" - case "created_at": - return "created_at asc" - case "created_at-desc": - return "created_at desc" - case "accessed_at": - return "accessed_at asc" - case "accessed_at-desc": - return "accessed_at desc" - case "status": - return "status asc" - case "status-desc": - return "status desc" - case "test_at": - return "test_at asc" - case "test_at-desc": - return "test_at desc" - case "balance_updated_at": - return "balance_updated_at asc" - case "balance_updated_at-desc": - return "balance_updated_at desc" - case "used_amount": - return "used_amount asc" - case "used_amount-desc": - return "used_amount desc" - case "request_count": - return "request_count asc" - case "request_count-desc": - return "request_count desc" - case "priority": - return "priority asc" - case "priority-desc": - return "priority desc" - case "id": - return "id asc" + prefix, suffix, _ := strings.Cut(order, "-") + switch prefix { + case "name", "type", "created_at", "accessed_at", "status", "test_at", "balance_updated_at", "used_amount", "request_count", "priority", "id": + switch suffix { + case "asc": + return prefix + " asc" + default: + return prefix + " desc" + } default: return "id desc" } @@ -120,7 +147,6 @@ type ChannelConfig struct { AK string `json:"ak,omitempty"` UserID string `json:"user_id,omitempty"` APIVersion string `json:"api_version,omitempty"` - LibraryID string `json:"library_id,omitempty"` Plugin string `json:"plugin,omitempty"` VertexAIProjectID string `json:"vertex_ai_project_id,omitempty"` VertexAIADC string `json:"vertex_ai_adc,omitempty"` @@ -260,10 +286,7 @@ func GetChannelByID(id int, omitKey bool) (*Channel, error) { } else { err = DB.First(&channel, "id = ?", id).Error } - if err != nil { - return nil, err - } - return &channel, nil + return &channel, HandleNotFound(err, ErrChannelNotFound) } func BatchInsertChannels(channels []*Channel) error { @@ -275,30 +298,59 @@ func BatchInsertChannels(channels []*Channel) error { func UpdateChannel(channel *Channel) error { result := DB. Model(channel). - Omit("accessed_at", "used_amount", "request_count", "balance_updated_at", "created_at", "balance", "test_at", "balance_updated_at"). + Omit("accessed_at", "used_amount", "request_count", "created_at", "balance_updated_at", "balance"). Clauses(clause.Returning{}). Updates(channel) return HandleUpdateResult(result, ErrChannelNotFound) } -func (c *Channel) UpdateResponseTime(responseTime int64) { - err := DB.Model(c).Select("test_at", "response_duration").Updates(Channel{ - TestAt: time.Now(), - ResponseDuration: responseTime, - }).Error +func ClearLastTestErrorAt(id int) error { + result := DB.Model(&Channel{}).Where("id = ?", id).Update("last_test_error_at", gorm.Expr("NULL")) + return HandleUpdateResult(result, ErrChannelNotFound) +} + +func (c *Channel) UpdateModelTest(testAt time.Time, model, actualModel string, mode int, took float64, success bool, response string, code int) (*ChannelTest, error) { + var ct *ChannelTest + err := DB.Transaction(func(tx *gorm.DB) error { + if !success { + result := tx.Model(&Channel{}).Where("id = ?", c.ID).Update("last_test_error_at", testAt) + if err := HandleUpdateResult(result, ErrChannelNotFound); err != nil { + return err + } + } else if !c.LastTestErrorAt.IsZero() && time.Since(c.LastTestErrorAt) > time.Hour { + result := tx.Model(&Channel{}).Where("id = ?", c.ID).Update("last_test_error_at", gorm.Expr("NULL")) + if err := HandleUpdateResult(result, ErrChannelNotFound); err != nil { + return err + } + } + ct = &ChannelTest{ + ChannelID: c.ID, + ChannelType: c.Type, + ChannelName: c.Name, + Model: model, + ActualModel: actualModel, + Mode: mode, + TestAt: testAt, + Took: took, + Success: success, + Response: response, + Code: code, + } + result := tx.Save(ct) + return HandleUpdateResult(result, ErrChannelNotFound) + }) if err != nil { - logger.SysError("failed to update response time: " + err.Error()) + return nil, err } + return ct, nil } -func (c *Channel) UpdateBalance(balance float64) { - err := DB.Model(c).Select("balance_updated_at", "balance").Updates(Channel{ +func (c *Channel) UpdateBalance(balance float64) error { + result := DB.Model(c).Select("balance_updated_at", "balance").Updates(Channel{ BalanceUpdatedAt: time.Now(), Balance: balance, - }).Error - if err != nil { - logger.SysError("failed to update balance: " + err.Error()) - } + }) + return HandleUpdateResult(result, ErrChannelNotFound) } func DeleteChannelByID(id int) error { @@ -306,6 +358,15 @@ func DeleteChannelByID(id int) error { return HandleUpdateResult(result, ErrChannelNotFound) } +func DeleteChannelsByIDs(ids []int) error { + return DB.Transaction(func(tx *gorm.DB) error { + return tx. + Where("id IN (?)", ids). + Delete(&Channel{}). + Error + }) +} + func UpdateChannelStatusByID(id int, status int) error { result := DB.Model(&Channel{}).Where("id = ?", id).Update("status", status) return HandleUpdateResult(result, ErrChannelNotFound) diff --git a/service/aiproxy/model/channeltest.go b/service/aiproxy/model/channeltest.go new file mode 100644 index 00000000000..c1d94f7ef23 --- /dev/null +++ b/service/aiproxy/model/channeltest.go @@ -0,0 +1,32 @@ +package model + +import ( + "time" + + json "github.com/json-iterator/go" +) + +type ChannelTest struct { + TestAt time.Time `json:"test_at"` + Model string `gorm:"primaryKey" json:"model"` + ActualModel string `json:"actual_model"` + Response string `gorm:"type:text" json:"response"` + ChannelName string `json:"channel_name"` + ChannelType int `json:"channel_type"` + ChannelID int `gorm:"primaryKey" json:"channel_id"` + Took float64 `json:"took"` + Success bool `json:"success"` + Mode int `json:"mode"` + Code int `json:"code"` +} + +func (ct *ChannelTest) MarshalJSON() ([]byte, error) { + type Alias ChannelTest + return json.Marshal(&struct { + *Alias + TestAt int64 `json:"test_at"` + }{ + Alias: (*Alias)(ct), + TestAt: ct.TestAt.UnixMilli(), + }) +} diff --git a/service/aiproxy/model/consumeerr.go b/service/aiproxy/model/consumeerr.go index 7bf76b9b49e..36e5f2b46f8 100644 --- a/service/aiproxy/model/consumeerr.go +++ b/service/aiproxy/model/consumeerr.go @@ -11,14 +11,16 @@ import ( ) type ConsumeError struct { - CreatedAt time.Time `gorm:"index" json:"created_at"` - GroupID string `gorm:"index" json:"group_id"` - TokenName EmptyNullString `gorm:"index;not null" json:"token_name"` - Model string `gorm:"index" json:"model"` - Content string `gorm:"type:text" json:"content"` - ID int `gorm:"primaryKey" json:"id"` - UsedAmount float64 `gorm:"index" json:"used_amount"` - TokenID int `gorm:"index" json:"token_id"` + RequestAt time.Time `gorm:"index;index:idx_consume_error_group_reqat,priority:2" json:"request_at"` + CreatedAt time.Time `json:"created_at"` + GroupID string `gorm:"index;index:idx_consume_error_group_reqat,priority:1" json:"group_id"` + RequestID string `gorm:"index" json:"request_id"` + TokenName EmptyNullString `gorm:"not null" json:"token_name"` + Model string `json:"model"` + Content string `gorm:"type:text" json:"content"` + ID int `gorm:"primaryKey" json:"id"` + UsedAmount float64 `json:"used_amount"` + TokenID int `json:"token_id"` } func (c *ConsumeError) MarshalJSON() ([]byte, error) { @@ -26,14 +28,18 @@ func (c *ConsumeError) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { *Alias CreatedAt int64 `json:"created_at"` + RequestAt int64 `json:"request_at"` }{ Alias: (*Alias)(c), CreatedAt: c.CreatedAt.UnixMilli(), + RequestAt: c.RequestAt.UnixMilli(), }) } -func CreateConsumeError(group string, tokenName string, model string, content string, usedAmount float64, tokenID int) error { +func CreateConsumeError(requestID string, requestAt time.Time, group string, tokenName string, model string, content string, usedAmount float64, tokenID int) error { return LogDB.Create(&ConsumeError{ + RequestID: requestID, + RequestAt: requestAt, GroupID: group, TokenName: EmptyNullString(tokenName), Model: model, @@ -43,13 +49,16 @@ func CreateConsumeError(group string, tokenName string, model string, content st }).Error } -func SearchConsumeError(keyword string, group string, tokenName string, model string, content string, usedAmount float64, tokenID int, page int, perPage int, order string) ([]*ConsumeError, int64, error) { +func SearchConsumeError(keyword string, requestID string, group string, tokenName string, model string, content string, usedAmount float64, tokenID int, page int, perPage int, order string) ([]*ConsumeError, int64, error) { tx := LogDB.Model(&ConsumeError{}) // Handle exact match conditions for non-zero values if group != "" { tx = tx.Where("group_id = ?", group) } + if requestID != "" { + tx = tx.Where("request_id = ?", requestID) + } if tokenName != "" { tx = tx.Where("token_name = ?", tokenName) } @@ -75,6 +84,14 @@ func SearchConsumeError(keyword string, group string, tokenName string, model st conditions = append(conditions, "token_id = ?") values = append(values, helper.String2Int(keyword)) } + if requestID == "" { + if common.UsingPostgreSQL { + conditions = append(conditions, "request_id ILIKE ?") + } else { + conditions = append(conditions, "request_id LIKE ?") + } + values = append(values, "%"+keyword+"%") + } if group == "" { if common.UsingPostgreSQL { conditions = append(conditions, "group_id ILIKE ?") diff --git a/service/aiproxy/model/group.go b/service/aiproxy/model/group.go index aaf7523f1e3..9e33745c43f 100644 --- a/service/aiproxy/model/group.go +++ b/service/aiproxy/model/group.go @@ -9,8 +9,9 @@ import ( json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" "gorm.io/gorm" + "gorm.io/gorm/clause" ) const ( @@ -33,7 +34,7 @@ type Group struct { RequestCount int `gorm:"index" json:"request_count"` } -func (g *Group) AfterDelete(tx *gorm.DB) (err error) { +func (g *Group) BeforeDelete(tx *gorm.DB) (err error) { return tx.Model(&Token{}).Where("group_id = ?", g.ID).Delete(&Token{}).Error } @@ -52,31 +53,15 @@ func (g *Group) MarshalJSON() ([]byte, error) { //nolint:goconst func getGroupOrder(order string) string { - switch order { - case "id-desc": - return "id desc" - case "request_count": - return "request_count asc" - case "request_count-desc": - return "request_count desc" - case "accessed_at": - return "accessed_at asc" - case "accessed_at-desc": - return "accessed_at desc" - case "status": - return "status asc" - case "status-desc": - return "status desc" - case "created_at": - return "created_at asc" - case "created_at-desc": - return "created_at desc" - case "used_amount": - return "used_amount asc" - case "used_amount-desc": - return "used_amount desc" - case "id": - return "id asc" + prefix, suffix, _ := strings.Cut(order, "-") + switch prefix { + case "id", "request_count", "accessed_at", "status", "created_at", "used_amount": + switch suffix { + case "asc": + return prefix + " asc" + default: + return prefix + " desc" + } default: return "id desc" } @@ -117,20 +102,47 @@ func DeleteGroupByID(id string) (err error) { defer func() { if err == nil { if err := CacheDeleteGroup(id); err != nil { - logger.SysError("CacheDeleteGroup failed: " + err.Error()) + log.Error("cache delete group failed: " + err.Error()) } if _, err := DeleteGroupLogs(id); err != nil { - logger.SysError("DeleteGroupLogs failed: " + err.Error()) + log.Error("delete group logs failed: " + err.Error()) } } }() - result := DB. - Delete(&Group{ - ID: id, - }) + result := DB.Delete(&Group{ID: id}) return HandleUpdateResult(result, ErrGroupNotFound) } +func DeleteGroupsByIDs(ids []string) (err error) { + if len(ids) == 0 { + return nil + } + groups := make([]Group, len(ids)) + defer func() { + if err == nil { + for _, group := range groups { + if err := CacheDeleteGroup(group.ID); err != nil { + log.Error("cache delete group failed: " + err.Error()) + } + if _, err := DeleteGroupLogs(group.ID); err != nil { + log.Error("delete group logs failed: " + err.Error()) + } + } + } + }() + return DB.Transaction(func(tx *gorm.DB) error { + return tx. + Clauses(clause.Returning{ + Columns: []clause.Column{ + {Name: "id"}, + }, + }). + Where("id IN (?)", ids). + Delete(&groups). + Error + }) +} + func UpdateGroupUsedAmountAndRequestCount(id string, amount float64, count int) error { result := DB.Model(&Group{}).Where("id = ?", id).Updates(map[string]interface{}{ "used_amount": gorm.Expr("used_amount + ?", amount), @@ -160,7 +172,7 @@ func UpdateGroupQPM(id string, qpm int64) (err error) { defer func() { if err == nil { if err := CacheUpdateGroupQPM(id, qpm); err != nil { - logger.SysError("CacheUpdateGroupQPM failed: " + err.Error()) + log.Error("cache update group qpm failed: " + err.Error()) } } }() @@ -172,7 +184,7 @@ func UpdateGroupStatus(id string, status int) (err error) { defer func() { if err == nil { if err := CacheUpdateGroupStatus(id, status); err != nil { - logger.SysError("CacheUpdateGroupStatus failed: " + err.Error()) + log.Error("cache update group status failed: " + err.Error()) } } }() diff --git a/service/aiproxy/model/log.go b/service/aiproxy/model/log.go index 15fe1f6807b..66cb6e95d17 100644 --- a/service/aiproxy/model/log.go +++ b/service/aiproxy/model/log.go @@ -1,34 +1,47 @@ package model import ( - "context" "errors" "fmt" "strings" "time" json "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/helper" ) +type RequestDetail struct { + CreatedAt time.Time `gorm:"autoCreateTime" json:"-"` + RequestBody string `gorm:"type:text" json:"request_body"` + ResponseBody string `gorm:"type:text" json:"response_body"` + ID int `json:"id"` + LogID int `json:"log_id"` +} + type Log struct { - CreatedAt time.Time `gorm:"index" json:"created_at"` - TokenName string `gorm:"index" json:"token_name"` - Endpoint string `gorm:"index" json:"endpoint"` - Content string `gorm:"type:text" json:"content"` - GroupID string `gorm:"index" json:"group"` - Model string `gorm:"index" json:"model"` - Price float64 `json:"price"` - ID int `gorm:"primaryKey" json:"id"` - CompletionPrice float64 `json:"completion_price"` - TokenID int `gorm:"index" json:"token_id"` - UsedAmount float64 `gorm:"index" json:"used_amount"` - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - ChannelID int `gorm:"index" json:"channel"` - Code int `gorm:"index" json:"code"` + RequestDetail *RequestDetail `gorm:"foreignKey:LogID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"request_detail,omitempty"` + RequestAt time.Time `gorm:"index;index:idx_request_at_group_id,priority:2;index:idx_group_reqat_token,priority:2" json:"request_at"` + CreatedAt time.Time `gorm:"index" json:"created_at"` + TokenName string `gorm:"index;index:idx_group_token,priority:2;index:idx_group_reqat_token,priority:3" json:"token_name"` + Endpoint string `gorm:"index" json:"endpoint"` + Content string `gorm:"type:text" json:"content"` + GroupID string `gorm:"index;index:idx_group_token,priority:1;index:idx_request_at_group_id,priority:1;index:idx_group_reqat_token,priority:1" json:"group"` + Model string `gorm:"index" json:"model"` + RequestID string `gorm:"index" json:"request_id"` + Price float64 `json:"price"` + ID int `gorm:"primaryKey" json:"id"` + CompletionPrice float64 `json:"completion_price"` + TokenID int `gorm:"index" json:"token_id"` + UsedAmount float64 `gorm:"index" json:"used_amount"` + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + ChannelID int `gorm:"index" json:"channel"` + Code int `gorm:"index" json:"code"` + Mode int `json:"mode"` } func (l *Log) MarshalJSON() ([]byte, error) { @@ -36,14 +49,48 @@ func (l *Log) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { *Alias CreatedAt int64 `json:"created_at"` + RequestAt int64 `json:"request_at"` }{ Alias: (*Alias)(l), CreatedAt: l.CreatedAt.UnixMilli(), + RequestAt: l.RequestAt.UnixMilli(), }) } -func RecordConsumeLog(_ context.Context, group string, code int, channelID int, promptTokens int, completionTokens int, modelName string, tokenID int, tokenName string, amount float64, price float64, completionPrice float64, endpoint string, content string) error { +func RecordConsumeLog( + requestID string, + requestAt time.Time, + group string, + code int, + channelID int, + promptTokens int, + completionTokens int, + modelName string, + tokenID int, + tokenName string, + amount float64, + price float64, + completionPrice float64, + endpoint string, + content string, + mode int, + requestDetail *RequestDetail, +) error { + defer func() { + detailStorageHours := config.GetLogDetailStorageHours() + if detailStorageHours <= 0 { + return + } + err := LogDB. + Where("created_at < ?", time.Now().Add(-time.Duration(detailStorageHours)*time.Hour)). + Delete(&RequestDetail{}).Error + if err != nil { + log.Errorf("delete request detail failed: %s", err) + } + }() log := &Log{ + RequestID: requestID, + RequestAt: requestAt, GroupID: group, CreatedAt: time.Now(), Code: code, @@ -52,87 +99,59 @@ func RecordConsumeLog(_ context.Context, group string, code int, channelID int, TokenID: tokenID, TokenName: tokenName, Model: modelName, + Mode: mode, UsedAmount: amount, Price: price, CompletionPrice: completionPrice, ChannelID: channelID, Endpoint: endpoint, Content: content, + RequestDetail: requestDetail, } return LogDB.Create(log).Error } //nolint:goconst func getLogOrder(order string) string { - switch order { - case "id-desc": - return "id desc" - case "used_amount": - return "used_amount asc" - case "used_amount-desc": - return "used_amount desc" - case "price": - return "price asc" - case "price-desc": - return "price desc" - case "completion_price": - return "completion_price asc" - case "completion_price-desc": - return "completion_price desc" - case "token_id": - return "token_id asc" - case "token_id-desc": - return "token_id desc" - case "token_name": - return "token_name asc" - case "token_name-desc": - return "token_name desc" - case "prompt_tokens": - return "prompt_tokens asc" - case "prompt_tokens-desc": - return "prompt_tokens desc" - case "completion_tokens": - return "completion_tokens asc" - case "completion_tokens-desc": - return "completion_tokens desc" - case "endpoint": - return "endpoint asc" - case "endpoint-desc": - return "endpoint desc" - case "group": - return "group_id asc" - case "group-desc": - return "group_id desc" - case "created_at": - return "created_at asc" - case "created_at-desc": - return "created_at desc" - case "id": - return "id asc" + prefix, suffix, _ := strings.Cut(order, "-") + switch prefix { + case "used_amount", "token_id", "token_name", "group", "request_id", "request_at", "id", "created_at": + switch suffix { + case "asc": + return prefix + " asc" + default: + return prefix + " desc" + } default: - return "id desc" + return "request_at desc" } } -func GetLogs(startTimestamp time.Time, endTimestamp time.Time, code int, modelName string, group string, tokenID int, tokenName string, startIdx int, num int, channelID int, endpoint string, content string, order string) (logs []*Log, total int64, err error) { +func GetLogs(startTimestamp time.Time, endTimestamp time.Time, code int, modelName string, group string, requestID string, tokenID int, tokenName string, startIdx int, num int, channelID int, endpoint string, content string, order string, mode int) (logs []*Log, total int64, err error) { tx := LogDB.Model(&Log{}) - if modelName != "" { - tx = tx.Where("model = ?", modelName) - } if group != "" { tx = tx.Where("group_id = ?", group) } - if tokenID != 0 { - tx = tx.Where("token_id = ?", tokenID) + if !startTimestamp.IsZero() { + tx = tx.Where("request_at >= ?", startTimestamp) + } + if !endTimestamp.IsZero() { + tx = tx.Where("request_at <= ?", endTimestamp) } if tokenName != "" { tx = tx.Where("token_name = ?", tokenName) } - if !startTimestamp.IsZero() { - tx = tx.Where("created_at >= ?", startTimestamp) + if requestID != "" { + tx = tx.Where("request_id = ?", requestID) } - if !endTimestamp.IsZero() { - tx = tx.Where("created_at <= ?", endTimestamp) + if modelName != "" { + tx = tx.Where("model = ?", modelName) + } + if mode != 0 { + tx = tx.Where("mode = ?", mode) + } + if tokenID != 0 { + tx = tx.Where("token_id = ?", tokenID) } if channelID != 0 { tx = tx.Where("channel_id = ?", channelID) @@ -154,26 +173,37 @@ func GetLogs(startTimestamp time.Time, endTimestamp time.Time, code int, modelNa return nil, 0, nil } - err = tx.Order(getLogOrder(order)).Limit(num).Offset(startIdx).Find(&logs).Error + err = tx. + Preload("RequestDetail"). + Order(getLogOrder(order)). + Limit(num). + Offset(startIdx). + Find(&logs).Error return logs, total, err } -func GetGroupLogs(group string, startTimestamp time.Time, endTimestamp time.Time, code int, modelName string, tokenID int, tokenName string, startIdx int, num int, channelID int, endpoint string, content string, order string) (logs []*Log, total int64, err error) { +func GetGroupLogs(group string, startTimestamp time.Time, endTimestamp time.Time, code int, modelName string, requestID string, tokenID int, tokenName string, startIdx int, num int, channelID int, endpoint string, content string, order string, mode int) (logs []*Log, total int64, err error) { tx := LogDB.Model(&Log{}).Where("group_id = ?", group) - if modelName != "" { - tx = tx.Where("model = ?", modelName) + if !startTimestamp.IsZero() { + tx = tx.Where("request_at >= ?", startTimestamp) } - if tokenID != 0 { - tx = tx.Where("token_id = ?", tokenID) + if !endTimestamp.IsZero() { + tx = tx.Where("request_at <= ?", endTimestamp) } if tokenName != "" { tx = tx.Where("token_name = ?", tokenName) } - if !startTimestamp.IsZero() { - tx = tx.Where("created_at >= ?", startTimestamp) + if modelName != "" { + tx = tx.Where("model = ?", modelName) } - if !endTimestamp.IsZero() { - tx = tx.Where("created_at <= ?", endTimestamp) + if mode != 0 { + tx = tx.Where("mode = ?", mode) + } + if requestID != "" { + tx = tx.Where("request_id = ?", requestID) + } + if tokenID != 0 { + tx = tx.Where("token_id = ?", tokenID) } if channelID != 0 { tx = tx.Where("channel_id = ?", channelID) @@ -195,25 +225,27 @@ func GetGroupLogs(group string, startTimestamp time.Time, endTimestamp time.Time return nil, 0, nil } - err = tx.Order(getLogOrder(order)).Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error + err = tx. + Preload("RequestDetail"). + Order(getLogOrder(order)). + Limit(num). + Offset(startIdx). + Find(&logs).Error return logs, total, err } -func SearchLogs(keyword string, page int, perPage int, code int, endpoint string, groupID string, tokenID int, tokenName string, modelName string, content string, startTimestamp time.Time, endTimestamp time.Time, channelID int, order string) (logs []*Log, total int64, err error) { +func SearchLogs(keyword string, page int, perPage int, code int, endpoint string, groupID string, requestID string, tokenID int, tokenName string, modelName string, content string, startTimestamp time.Time, endTimestamp time.Time, channelID int, order string, mode int) (logs []*Log, total int64, err error) { tx := LogDB.Model(&Log{}) // Handle exact match conditions for non-zero values - if code != 0 { - tx = tx.Where("code = ?", code) - } - if endpoint != "" { - tx = tx.Where("endpoint = ?", endpoint) - } if groupID != "" { tx = tx.Where("group_id = ?", groupID) } - if tokenID != 0 { - tx = tx.Where("token_id = ?", tokenID) + if !startTimestamp.IsZero() { + tx = tx.Where("request_at >= ?", startTimestamp) + } + if !endTimestamp.IsZero() { + tx = tx.Where("request_at <= ?", endTimestamp) } if tokenName != "" { tx = tx.Where("token_name = ?", tokenName) @@ -221,14 +253,23 @@ func SearchLogs(keyword string, page int, perPage int, code int, endpoint string if modelName != "" { tx = tx.Where("model = ?", modelName) } - if content != "" { - tx = tx.Where("content = ?", content) + if mode != 0 { + tx = tx.Where("mode = ?", mode) } - if !startTimestamp.IsZero() { - tx = tx.Where("created_at >= ?", startTimestamp) + if tokenID != 0 { + tx = tx.Where("token_id = ?", tokenID) } - if !endTimestamp.IsZero() { - tx = tx.Where("created_at <= ?", endTimestamp) + if code != 0 { + tx = tx.Where("code = ?", code) + } + if endpoint != "" { + tx = tx.Where("endpoint = ?", endpoint) + } + if requestID != "" { + tx = tx.Where("request_id = ?", requestID) + } + if content != "" { + tx = tx.Where("content = ?", content) } if channelID != 0 { tx = tx.Where("channel_id = ?", channelID) @@ -239,14 +280,21 @@ func SearchLogs(keyword string, page int, perPage int, code int, endpoint string var conditions []string var values []interface{} - if code == 0 { - conditions = append(conditions, "code = ?") - values = append(values, helper.String2Int(keyword)) - } - if channelID == 0 { - conditions = append(conditions, "channel_id = ?") - values = append(values, helper.String2Int(keyword)) + if num := helper.String2Int(keyword); num != 0 { + if code == 0 { + conditions = append(conditions, "code = ?") + values = append(values, num) + } + if channelID == 0 { + conditions = append(conditions, "channel_id = ?") + values = append(values, num) + } + if mode != 0 { + conditions = append(conditions, "mode = ?") + values = append(values, num) + } } + if endpoint == "" { if common.UsingPostgreSQL { conditions = append(conditions, "endpoint ILIKE ?") @@ -263,6 +311,14 @@ func SearchLogs(keyword string, page int, perPage int, code int, endpoint string } values = append(values, "%"+keyword+"%") } + if requestID == "" { + if common.UsingPostgreSQL { + conditions = append(conditions, "request_id ILIKE ?") + } else { + conditions = append(conditions, "request_id LIKE ?") + } + values = append(values, "%"+keyword+"%") + } if tokenName == "" { if common.UsingPostgreSQL { conditions = append(conditions, "token_name ILIKE ?") @@ -305,41 +361,52 @@ func SearchLogs(keyword string, page int, perPage int, code int, endpoint string if page < 0 { page = 0 } - err = tx.Order(getLogOrder(order)).Limit(perPage).Offset(page * perPage).Find(&logs).Error + err = tx. + Preload("RequestDetail"). + Order(getLogOrder(order)). + Limit(perPage). + Offset(page * perPage). + Find(&logs).Error return logs, total, err } -func SearchGroupLogs(group string, keyword string, page int, perPage int, code int, endpoint string, tokenID int, tokenName string, modelName string, content string, startTimestamp time.Time, endTimestamp time.Time, channelID int, order string) (logs []*Log, total int64, err error) { +func SearchGroupLogs(group string, keyword string, page int, perPage int, code int, endpoint string, requestID string, tokenID int, tokenName string, modelName string, content string, startTimestamp time.Time, endTimestamp time.Time, channelID int, order string, mode int) (logs []*Log, total int64, err error) { if group == "" { return nil, 0, errors.New("group is empty") } tx := LogDB.Model(&Log{}).Where("group_id = ?", group) // Handle exact match conditions for non-zero values + if !startTimestamp.IsZero() { + tx = tx.Where("request_at >= ?", startTimestamp) + } + if !endTimestamp.IsZero() { + tx = tx.Where("request_at <= ?", endTimestamp) + } + if tokenName != "" { + tx = tx.Where("token_name = ?", tokenName) + } + if modelName != "" { + tx = tx.Where("model = ?", modelName) + } if code != 0 { tx = tx.Where("code = ?", code) } + if mode != 0 { + tx = tx.Where("mode = ?", mode) + } if endpoint != "" { tx = tx.Where("endpoint = ?", endpoint) } + if requestID != "" { + tx = tx.Where("request_id = ?", requestID) + } if tokenID != 0 { tx = tx.Where("token_id = ?", tokenID) } - if tokenName != "" { - tx = tx.Where("token_name = ?", tokenName) - } - if modelName != "" { - tx = tx.Where("model = ?", modelName) - } if content != "" { tx = tx.Where("content = ?", content) } - if !startTimestamp.IsZero() { - tx = tx.Where("created_at >= ?", startTimestamp) - } - if !endTimestamp.IsZero() { - tx = tx.Where("created_at <= ?", endTimestamp) - } if channelID != 0 { tx = tx.Where("channel_id = ?", channelID) } @@ -349,13 +416,19 @@ func SearchGroupLogs(group string, keyword string, page int, perPage int, code i var conditions []string var values []interface{} - if code == 0 { - conditions = append(conditions, "code = ?") - values = append(values, helper.String2Int(keyword)) - } - if channelID == 0 { - conditions = append(conditions, "channel_id = ?") - values = append(values, helper.String2Int(keyword)) + if num := helper.String2Int(keyword); num != 0 { + if code == 0 { + conditions = append(conditions, "code = ?") + values = append(values, num) + } + if channelID == 0 { + conditions = append(conditions, "channel_id = ?") + values = append(values, num) + } + if mode != 0 { + conditions = append(conditions, "mode = ?") + values = append(values, num) + } } if endpoint == "" { if common.UsingPostgreSQL { @@ -365,6 +438,14 @@ func SearchGroupLogs(group string, keyword string, page int, perPage int, code i } values = append(values, "%"+keyword+"%") } + if requestID == "" { + if common.UsingPostgreSQL { + conditions = append(conditions, "request_id ILIKE ?") + } else { + conditions = append(conditions, "request_id LIKE ?") + } + values = append(values, "%"+keyword+"%") + } if tokenName == "" { if common.UsingPostgreSQL { conditions = append(conditions, "token_name ILIKE ?") @@ -408,7 +489,12 @@ func SearchGroupLogs(group string, keyword string, page int, perPage int, code i page = 0 } - err = tx.Order(getLogOrder(order)).Limit(perPage).Offset(page * perPage).Find(&logs).Error + err = tx. + Preload("RequestDetail"). + Order(getLogOrder(order)). + Limit(perPage). + Offset(page * perPage). + Find(&logs).Error return logs, total, err } @@ -425,10 +511,10 @@ func SumUsedQuota(startTimestamp time.Time, endTimestamp time.Time, modelName st tx = tx.Where("token_name = ?", tokenName) } if !startTimestamp.IsZero() { - tx = tx.Where("created_at >= ?", startTimestamp) + tx = tx.Where("request_at >= ?", startTimestamp) } if !endTimestamp.IsZero() { - tx = tx.Where("created_at <= ?", endTimestamp) + tx = tx.Where("request_at <= ?", endTimestamp) } if modelName != "" { tx = tx.Where("model = ?", modelName) @@ -456,10 +542,10 @@ func SumUsedToken(startTimestamp time.Time, endTimestamp time.Time, modelName st tx = tx.Where("token_name = ?", tokenName) } if !startTimestamp.IsZero() { - tx = tx.Where("created_at >= ?", startTimestamp) + tx = tx.Where("request_at >= ?", startTimestamp) } if !endTimestamp.IsZero() { - tx = tx.Where("created_at <= ?", endTimestamp) + tx = tx.Where("request_at <= ?", endTimestamp) } if modelName != "" { tx = tx.Where("model = ?", modelName) @@ -472,7 +558,7 @@ func SumUsedToken(startTimestamp time.Time, endTimestamp time.Time, modelName st } func DeleteOldLog(timestamp time.Time) (int64, error) { - result := LogDB.Where("created_at < ?", timestamp).Delete(&Log{}) + result := LogDB.Where("request_at < ?", timestamp).Delete(&Log{}) return result.RowsAffected, result.Error } diff --git a/service/aiproxy/model/main.go b/service/aiproxy/model/main.go index 2009f9a08ae..50c54c3cfdf 100644 --- a/service/aiproxy/model/main.go +++ b/service/aiproxy/model/main.go @@ -2,7 +2,6 @@ package model import ( "fmt" - "log" "os" "strings" "time" @@ -14,7 +13,7 @@ import ( // import fastjson serializer _ "github.com/labring/sealos/service/aiproxy/common/fastJSONSerializer" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -50,19 +49,19 @@ func newDBLogger() gormLogger.Interface { logLevel = gormLogger.Warn } return gormLogger.New( - log.New(os.Stdout, "", log.LstdFlags), + log.StandardLogger(), gormLogger.Config{ SlowThreshold: time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, ParameterizedQueries: !config.DebugSQLEnabled, - Colorful: true, + Colorful: common.NeedColor(), }, ) } func openPostgreSQL(dsn string) (*gorm.DB, error) { - logger.SysLog("using PostgreSQL as database") + log.Info("using PostgreSQL as database") common.UsingPostgreSQL = true return gorm.Open(postgres.New(postgres.Config{ DSN: dsn, @@ -77,7 +76,7 @@ func openPostgreSQL(dsn string) (*gorm.DB, error) { } func openMySQL(dsn string) (*gorm.DB, error) { - logger.SysLog("using MySQL as database") + log.Info("using MySQL as database") common.UsingMySQL = true return gorm.Open(mysql.Open(dsn), &gorm.Config{ PrepareStmt: true, // precompile SQL @@ -89,7 +88,7 @@ func openMySQL(dsn string) (*gorm.DB, error) { } func openSQLite() (*gorm.DB, error) { - logger.SysLog("SQL_DSN not set, using SQLite as database") + log.Info("SQL_DSN not set, using SQLite as database") common.UsingSQLite = true dsn := fmt.Sprintf("%s?_busy_timeout=%d", common.SQLitePath, common.SQLiteBusyTimeout) return gorm.Open(sqlite.Open(dsn), &gorm.Config{ @@ -105,7 +104,7 @@ func InitDB() { var err error DB, err = chooseDB("SQL_DSN") if err != nil { - logger.FatalLog("failed to initialize database: " + err.Error()) + log.Fatal("failed to initialize database: " + err.Error()) return } @@ -115,20 +114,22 @@ func InitDB() { return } - logger.SysLog("database migration started") + log.Info("database migration started") if err = migrateDB(); err != nil { - logger.FatalLog("failed to migrate database: " + err.Error()) + log.Fatal("failed to migrate database: " + err.Error()) return } - logger.SysLog("database migrated") + log.Info("database migrated") } func migrateDB() error { err := DB.AutoMigrate( &Channel{}, + &ChannelTest{}, &Token{}, &Group{}, &Option{}, + &ModelConfig{}, ) if err != nil { return err @@ -144,18 +145,18 @@ func InitLogDB() { } err := migrateLOGDB() if err != nil { - logger.FatalLog("failed to migrate secondary database: " + err.Error()) + log.Fatal("failed to migrate secondary database: " + err.Error()) return } - logger.SysLog("secondary database migrated") + log.Info("secondary database migrated") return } - logger.SysLog("using secondary database for table logs") + log.Info("using secondary database for table logs") var err error LogDB, err = chooseDB("LOG_SQL_DSN") if err != nil { - logger.FatalLog("failed to initialize secondary database: " + err.Error()) + log.Fatal("failed to initialize secondary database: " + err.Error()) return } @@ -165,18 +166,19 @@ func InitLogDB() { return } - logger.SysLog("secondary database migration started") + log.Info("secondary database migration started") err = migrateLOGDB() if err != nil { - logger.FatalLog("failed to migrate secondary database: " + err.Error()) + log.Fatal("failed to migrate secondary database: " + err.Error()) return } - logger.SysLog("secondary database migrated") + log.Info("secondary database migrated") } func migrateLOGDB() error { return LogDB.AutoMigrate( &Log{}, + &RequestDetail{}, &ConsumeError{}, ) } @@ -188,7 +190,7 @@ func setDBConns(db *gorm.DB) { sqlDB, err := db.DB() if err != nil { - logger.FatalLog("failed to connect database: " + err.Error()) + log.Fatal("failed to connect database: " + err.Error()) return } diff --git a/service/aiproxy/model/modelconfig.go b/service/aiproxy/model/modelconfig.go new file mode 100644 index 00000000000..df8c92528a6 --- /dev/null +++ b/service/aiproxy/model/modelconfig.go @@ -0,0 +1,197 @@ +package model + +import ( + "fmt" + "strings" + "time" + + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "gorm.io/gorm" +) + +//nolint:revive +type ModelConfigKey string + +const ( + ModelConfigMaxContextTokensKey ModelConfigKey = "max_context_tokens" + ModelConfigMaxInputTokensKey ModelConfigKey = "max_input_tokens" + ModelConfigMaxOutputTokensKey ModelConfigKey = "max_output_tokens" + ModelConfigToolChoiceKey ModelConfigKey = "tool_choice" + ModelConfigFunctionCallingKey ModelConfigKey = "function_calling" + ModelConfigSupportFormatsKey ModelConfigKey = "support_formats" + ModelConfigSupportVoicesKey ModelConfigKey = "support_voices" +) + +//nolint:revive +type ModelOwner string + +const ( + ModelOwnerOpenAI ModelOwner = "openai" + ModelOwnerAlibaba ModelOwner = "alibaba" + ModelOwnerTencent ModelOwner = "tencent" + ModelOwnerXunfei ModelOwner = "xunfei" + ModelOwnerDeepSeek ModelOwner = "deepseek" + ModelOwnerMoonshot ModelOwner = "moonshot" + ModelOwnerMiniMax ModelOwner = "minimax" + ModelOwnerBaidu ModelOwner = "baidu" + ModelOwnerGoogle ModelOwner = "google" + ModelOwnerBAAI ModelOwner = "baai" + ModelOwnerFunAudioLLM ModelOwner = "funaudiollm" + ModelOwnerDoubao ModelOwner = "doubao" + ModelOwnerFishAudio ModelOwner = "fishaudio" + ModelOwnerChatGLM ModelOwner = "chatglm" + ModelOwnerStabilityAI ModelOwner = "stabilityai" + ModelOwnerNetease ModelOwner = "netease" + ModelOwnerAI360 ModelOwner = "ai360" + ModelOwnerAnthropic ModelOwner = "anthropic" + ModelOwnerMeta ModelOwner = "meta" + ModelOwnerBaichuan ModelOwner = "baichuan" + ModelOwnerMistral ModelOwner = "mistral" + ModelOwnerOpenChat ModelOwner = "openchat" + ModelOwnerMicrosoft ModelOwner = "microsoft" + ModelOwnerDefog ModelOwner = "defog" + ModelOwnerNexusFlow ModelOwner = "nexusflow" + ModelOwnerCohere ModelOwner = "cohere" + ModelOwnerHuggingFace ModelOwner = "huggingface" + ModelOwnerLingyiWanwu ModelOwner = "lingyiwanwu" + ModelOwnerStepFun ModelOwner = "stepfun" +) + +//nolint:revive +type ModelConfig struct { + CreatedAt time.Time `gorm:"index;autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"index;autoUpdateTime" json:"updated_at"` + Config map[ModelConfigKey]any `gorm:"serializer:fastjson;type:text" json:"config,omitempty"` + ImagePrices map[string]float64 `gorm:"serializer:fastjson" json:"image_prices"` + Model string `gorm:"primaryKey" json:"model"` + Owner ModelOwner `gorm:"type:varchar(255);index" json:"owner"` + ImageMaxBatchSize int `json:"image_batch_size"` + // relaymode/define.go + Type int `json:"type"` + InputPrice float64 `json:"input_price"` + OutputPrice float64 `json:"output_price"` +} + +func (c *ModelConfig) MarshalJSON() ([]byte, error) { + type Alias ModelConfig + return json.Marshal(&struct { + *Alias + CreatedAt int64 `json:"created_at,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + }{ + Alias: (*Alias)(c), + CreatedAt: c.CreatedAt.UnixMilli(), + UpdatedAt: c.UpdatedAt.UnixMilli(), + }) +} + +func GetModelConfigs(startIdx int, num int, model string) (configs []*ModelConfig, total int64, err error) { + tx := DB.Model(&ModelConfig{}) + if model != "" { + tx = tx.Where("model = ?", model) + } + err = tx.Count(&total).Error + if err != nil { + return nil, 0, err + } + if total <= 0 { + return nil, 0, nil + } + err = tx.Order("created_at desc").Limit(num).Offset(startIdx).Find(&configs).Error + return configs, total, err +} + +func GetAllModelConfigs() (configs []*ModelConfig, err error) { + tx := DB.Model(&ModelConfig{}) + err = tx.Order("created_at desc").Find(&configs).Error + return configs, err +} + +func GetModelConfigsByModels(models []string) (configs []*ModelConfig, err error) { + tx := DB.Model(&ModelConfig{}).Where("model IN (?)", models) + err = tx.Order("created_at desc").Find(&configs).Error + return configs, err +} + +func GetModelConfig(model string) (*ModelConfig, error) { + config := &ModelConfig{} + err := DB.Model(&ModelConfig{}).Where("model = ?", model).First(config).Error + return config, HandleNotFound(err, ErrModelConfigNotFound) +} + +func SearchModelConfigs(keyword string, startIdx int, num int, model string, owner ModelOwner) (configs []*ModelConfig, total int64, err error) { + tx := DB.Model(&ModelConfig{}).Where("model LIKE ?", "%"+keyword+"%") + if model != "" { + tx = tx.Where("model = ?", model) + } + if owner != "" { + tx = tx.Where("owner = ?", owner) + } + if keyword != "" { + var conditions []string + var values []interface{} + + if model == "" { + if common.UsingPostgreSQL { + conditions = append(conditions, "model ILIKE ?") + } else { + conditions = append(conditions, "model LIKE ?") + } + values = append(values, "%"+keyword+"%") + } + + if owner != "" { + if common.UsingPostgreSQL { + conditions = append(conditions, "owner ILIKE ?") + } else { + conditions = append(conditions, "owner LIKE ?") + } + values = append(values, "%"+string(owner)+"%") + } + + if len(conditions) > 0 { + tx = tx.Where(fmt.Sprintf("(%s)", strings.Join(conditions, " OR ")), values...) + } + } + err = tx.Count(&total).Error + if err != nil { + return nil, 0, err + } + if total <= 0 { + return nil, 0, nil + } + err = tx.Order("created_at desc").Limit(num).Offset(startIdx).Find(&configs).Error + return configs, total, err +} + +func SaveModelConfig(config *ModelConfig) error { + return DB.Save(config).Error +} + +func SaveModelConfigs(configs []*ModelConfig) error { + return DB.Transaction(func(tx *gorm.DB) error { + for _, config := range configs { + if err := tx.Save(config).Error; err != nil { + return err + } + } + return nil + }) +} + +const ErrModelConfigNotFound = "model config" + +func DeleteModelConfig(model string) error { + result := DB.Where("model = ?", model).Delete(&ModelConfig{}) + return HandleUpdateResult(result, ErrModelConfigNotFound) +} + +func DeleteModelConfigsByModels(models []string) error { + return DB.Transaction(func(tx *gorm.DB) error { + return tx. + Where("model IN (?)", models). + Delete(&ModelConfig{}). + Error + }) +} diff --git a/service/aiproxy/model/option.go b/service/aiproxy/model/option.go index d530d63286d..8be3fcb6854 100644 --- a/service/aiproxy/model/option.go +++ b/service/aiproxy/model/option.go @@ -1,16 +1,20 @@ package model import ( + "context" "errors" + "fmt" + "slices" + "sort" "strconv" + "sync" "time" json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/logger" - billingprice "github.com/labring/sealos/service/aiproxy/relay/price" + log "github.com/sirupsen/logrus" ) type Option struct { @@ -18,22 +22,21 @@ type Option struct { Value string `json:"value"` } -func AllOption() ([]*Option, error) { +func GetAllOption() ([]*Option, error) { var options []*Option err := DB.Find(&options).Error return options, err } -func InitOptionMap() { +func InitOptionMap() error { config.OptionMapRWMutex.Lock() config.OptionMap = make(map[string]string) + config.OptionMap["LogDetailStorageHours"] = strconv.FormatInt(config.GetLogDetailStorageHours(), 10) config.OptionMap["DisableServe"] = strconv.FormatBool(config.GetDisableServe()) config.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(config.GetAutomaticDisableChannelEnabled()) config.OptionMap["AutomaticEnableChannelWhenTestSucceedEnabled"] = strconv.FormatBool(config.GetAutomaticEnableChannelWhenTestSucceedEnabled()) config.OptionMap["ApproximateTokenEnabled"] = strconv.FormatBool(config.GetApproximateTokenEnabled()) - config.OptionMap["BillingEnabled"] = strconv.FormatBool(billingprice.GetBillingEnabled()) - config.OptionMap["ModelPrice"] = billingprice.ModelPrice2JSONString() - config.OptionMap["CompletionPrice"] = billingprice.CompletionPrice2JSONString() + config.OptionMap["BillingEnabled"] = strconv.FormatBool(config.GetBillingEnabled()) config.OptionMap["RetryTimes"] = strconv.FormatInt(config.GetRetryTimes(), 10) config.OptionMap["GlobalApiRateLimitNum"] = strconv.FormatInt(config.GetGlobalAPIRateLimitNum(), 10) config.OptionMap["DefaultGroupQPM"] = strconv.FormatInt(config.GetDefaultGroupQPM(), 10) @@ -45,46 +48,71 @@ func InitOptionMap() { config.OptionMap["GeminiVersion"] = config.GetGeminiVersion() config.OptionMap["GroupMaxTokenNum"] = strconv.FormatInt(int64(config.GetGroupMaxTokenNum()), 10) config.OptionMapRWMutex.Unlock() - loadOptionsFromDatabase() + err := loadOptionsFromDatabase(true) + if err != nil { + return err + } + return storeOptionMap() } -func loadOptionsFromDatabase() { - options, _ := AllOption() - for _, option := range options { - if option.Key == "ModelPrice" { - option.Value = billingprice.AddNewMissingPrice(option.Value) - } - err := updateOptionMap(option.Key, option.Value) +func storeOptionMap() error { + config.OptionMapRWMutex.Lock() + defer config.OptionMapRWMutex.Unlock() + for key, value := range config.OptionMap { + err := saveOption(key, value) if err != nil { - logger.SysError("failed to update option map: " + err.Error()) + return err + } + } + return nil +} + +func loadOptionsFromDatabase(isInit bool) error { + options, err := GetAllOption() + if err != nil { + return err + } + for _, option := range options { + err := updateOptionMap(option.Key, option.Value, isInit) + if err != nil && !errors.Is(err, ErrUnknownOptionKey) { + log.Errorf("failed to update option: %s, value: %s, error: %s", option.Key, option.Value, err.Error()) } } - logger.SysDebug("options synced from database") + return nil } -func SyncOptions(frequency time.Duration) { +func SyncOptions(ctx context.Context, wg *sync.WaitGroup, frequency time.Duration) { + defer wg.Done() + ticker := time.NewTicker(frequency) defer ticker.Stop() - for range ticker.C { - logger.SysDebug("syncing options from database") - loadOptionsFromDatabase() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := loadOptionsFromDatabase(true); err != nil { + log.Error("failed to sync options from database: " + err.Error()) + } + } } } -func UpdateOption(key string, value string) error { - err := updateOptionMap(key, value) - if err != nil { - return err - } - // Save to database first +func saveOption(key string, value string) error { option := Option{ - Key: key, + Key: key, + Value: value, } - err = DB.Assign(Option{Key: key, Value: value}).FirstOrCreate(&option).Error + result := DB.Save(&option) + return HandleUpdateResult(result, "option:"+key) +} + +func UpdateOption(key string, value string) error { + err := updateOptionMap(key, value, false) if err != nil { return err } - return nil + return saveOption(key, value) } func UpdateOptions(options map[string]string) error { @@ -108,11 +136,17 @@ func isTrue(value string) bool { return result } -func updateOptionMap(key string, value string) (err error) { +func updateOptionMap(key string, value string, isInit bool) (err error) { config.OptionMapRWMutex.Lock() defer config.OptionMapRWMutex.Unlock() config.OptionMap[key] = value switch key { + case "LogDetailStorageHours": + logDetailStorageHours, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + config.SetLogDetailStorageHours(logDetailStorageHours) case "DisableServe": config.SetDisableServe(isTrue(value)) case "AutomaticDisableChannelEnabled": @@ -122,7 +156,7 @@ func updateOptionMap(key string, value string) (err error) { case "ApproximateTokenEnabled": config.SetApproximateTokenEnabled(isTrue(value)) case "BillingEnabled": - billingprice.SetBillingEnabled(isTrue(value)) + config.SetBillingEnabled(isTrue(value)) case "GroupMaxTokenNum": groupMaxTokenNum, err := strconv.ParseInt(value, 10, 32) if err != nil { @@ -146,12 +180,43 @@ func updateOptionMap(key string, value string) (err error) { } config.SetDefaultGroupQPM(defaultGroupQPM) case "DefaultChannelModels": - var newModules map[int][]string - err := json.Unmarshal(conv.StringToBytes(value), &newModules) + var newModels map[int][]string + err := json.Unmarshal(conv.StringToBytes(value), &newModels) + if err != nil { + return err + } + // check model config exist + allModelsMap := make(map[string]struct{}) + for _, models := range newModels { + for _, model := range models { + allModelsMap[model] = struct{}{} + } + } + allModels := make([]string, 0, len(allModelsMap)) + for model := range allModelsMap { + allModels = append(allModels, model) + } + foundModels, missingModels, err := CheckModelConfig(allModels) if err != nil { return err } - config.SetDefaultChannelModels(newModules) + if !isInit && len(missingModels) > 0 { + sort.Strings(missingModels) + return fmt.Errorf("model config not found: %v", missingModels) + } + if len(missingModels) > 0 { + sort.Strings(missingModels) + log.Errorf("model config not found: %v", missingModels) + } + allowedNewModels := make(map[int][]string) + for t, ms := range newModels { + for _, m := range ms { + if slices.Contains(foundModels, m) { + allowedNewModels[t] = append(allowedNewModels[t], m) + } + } + } + config.SetDefaultChannelModels(allowedNewModels) case "DefaultChannelModelMapping": var newMapping map[int]map[string]string err := json.Unmarshal(conv.StringToBytes(value), &newMapping) @@ -165,10 +230,6 @@ func updateOptionMap(key string, value string) (err error) { return err } config.SetRetryTimes(retryTimes) - case "ModelPrice": - err = billingprice.UpdateModelPriceByJSONString(value) - case "CompletionPrice": - err = billingprice.UpdateCompletionPriceByJSONString(value) default: return ErrUnknownOptionKey } diff --git a/service/aiproxy/model/token.go b/service/aiproxy/model/token.go index 66485de4fd9..aa8a915b8bf 100644 --- a/service/aiproxy/model/token.go +++ b/service/aiproxy/model/token.go @@ -10,7 +10,7 @@ import ( "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/common/logger" + log "github.com/sirupsen/logrus" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -60,33 +60,15 @@ func (t *Token) MarshalJSON() ([]byte, error) { //nolint:goconst func getTokenOrder(order string) string { - switch order { - case "name": - return "name asc" - case "name-desc": - return "name desc" - case "accessed_at": - return "accessed_at asc" - case "accessed_at-desc": - return "accessed_at desc" - case "expired_at": - return "expired_at asc" - case "expired_at-desc": - return "expired_at desc" - case "group": - return "group_id asc" - case "group-desc": - return "group_id desc" - case "used_amount": - return "used_amount asc" - case "used_amount-desc": - return "used_amount desc" - case "request_count": - return "request_count asc" - case "request_count-desc": - return "request_count desc" - case "id": - return "id asc" + prefix, suffix, _ := strings.Cut(order, "-") + switch prefix { + case "name", "accessed_at", "expired_at", "group", "used_amount", "request_count", "id", "created_at": + switch suffix { + case "asc": + return prefix + " asc" + default: + return prefix + " desc" + } default: return "id desc" } @@ -309,7 +291,7 @@ func ValidateAndGetToken(key string) (token *TokenCache, err error) { } token, err = CacheGetTokenByKey(key) if err != nil { - logger.SysError("get token from cache failed: " + err.Error()) + log.Error("get token from cache failed: " + err.Error()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("invalid token") } @@ -327,7 +309,7 @@ func ValidateAndGetToken(key string) (token *TokenCache, err error) { if !time.Time(token.ExpiredAt).IsZero() && time.Time(token.ExpiredAt).Before(time.Now()) { err := UpdateTokenStatusAndAccessedAt(token.ID, TokenStatusExpired) if err != nil { - logger.SysError("failed to update token status" + err.Error()) + log.Error("failed to update token status" + err.Error()) } return nil, fmt.Errorf("token (%s[%d]) is expired", token.Name, token.ID) } @@ -335,7 +317,7 @@ func ValidateAndGetToken(key string) (token *TokenCache, err error) { // in this case, we can make sure the token is exhausted err := UpdateTokenStatusAndAccessedAt(token.ID, TokenStatusExhausted) if err != nil { - logger.SysError("failed to update token status" + err.Error()) + log.Error("failed to update token status" + err.Error()) } return nil, fmt.Errorf("token (%s[%d]) quota is exhausted", token.Name, token.ID) } @@ -367,7 +349,7 @@ func UpdateTokenStatus(id int, status int) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -392,7 +374,7 @@ func UpdateTokenStatusAndAccessedAt(id int, status int) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -417,7 +399,7 @@ func UpdateGroupTokenStatusAndAccessedAt(group string, id int, status int) (err defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -443,7 +425,7 @@ func UpdateGroupTokenStatus(group string, id int, status int) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -463,7 +445,7 @@ func UpdateGroupTokenStatus(group string, id int, status int) (err error) { return HandleUpdateResult(result, ErrTokenNotFound) } -func DeleteTokenByIDAndGroupID(id int, groupID string) (err error) { +func DeleteGroupTokenByID(groupID string, id int) (err error) { if id == 0 || groupID == "" { return errors.New("id 或 group 为空!") } @@ -471,7 +453,7 @@ func DeleteTokenByIDAndGroupID(id int, groupID string) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -486,6 +468,34 @@ func DeleteTokenByIDAndGroupID(id int, groupID string) (err error) { return HandleUpdateResult(result, ErrTokenNotFound) } +func DeleteGroupTokensByIDs(groupID string, ids []int) (err error) { + if len(ids) == 0 { + return nil + } + tokens := make([]Token, len(ids)) + defer func() { + if err == nil { + for _, token := range tokens { + if err := CacheDeleteToken(token.Key); err != nil { + log.Error("delete token from cache failed: " + err.Error()) + } + } + } + }() + return DB.Transaction(func(tx *gorm.DB) error { + return tx. + Clauses(clause.Returning{ + Columns: []clause.Column{ + {Name: "key"}, + }, + }). + Where("group_id = ?", groupID). + Where("id IN (?)", ids). + Delete(&tokens). + Error + }) +} + func DeleteTokenByID(id int) (err error) { if id == 0 { return errors.New("id 为空!") @@ -494,7 +504,7 @@ func DeleteTokenByID(id int) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -509,11 +519,38 @@ func DeleteTokenByID(id int) (err error) { return HandleUpdateResult(result, ErrTokenNotFound) } +func DeleteTokensByIDs(ids []int) (err error) { + if len(ids) == 0 { + return nil + } + tokens := make([]Token, len(ids)) + defer func() { + if err == nil { + for _, token := range tokens { + if err := CacheDeleteToken(token.Key); err != nil { + log.Error("delete token from cache failed: " + err.Error()) + } + } + } + }() + return DB.Transaction(func(tx *gorm.DB) error { + return tx. + Clauses(clause.Returning{ + Columns: []clause.Column{ + {Name: "key"}, + }, + }). + Where("id IN (?)", ids). + Delete(&tokens). + Error + }) +} + func UpdateToken(token *Token) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -531,7 +568,7 @@ func UpdateTokenUsedAmount(id int, amount float64, requestCount int) (err error) defer func() { if amount > 0 && err == nil && token.Quota > 0 { if err := CacheUpdateTokenUsedAmountOnlyIncrease(token.Key, token.UsedAmount); err != nil { - logger.SysError("update token used amount in cache failed: " + err.Error()) + log.Error("update token used amount in cache failed: " + err.Error()) } } }() @@ -560,7 +597,7 @@ func UpdateTokenName(id int, name string) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() @@ -584,7 +621,7 @@ func UpdateGroupTokenName(group string, id int, name string) (err error) { defer func() { if err == nil { if err := CacheDeleteToken(token.Key); err != nil { - logger.SysError("delete token from cache failed: " + err.Error()) + log.Error("delete token from cache failed: " + err.Error()) } } }() diff --git a/service/aiproxy/model/utils.go b/service/aiproxy/model/utils.go index 7868ef2338d..a12d0cae9b3 100644 --- a/service/aiproxy/model/utils.go +++ b/service/aiproxy/model/utils.go @@ -1,11 +1,11 @@ package model import ( - "context" "database/sql/driver" "errors" "fmt" "strings" + "time" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -41,9 +41,45 @@ func OnConflictDoNothing() *gorm.DB { }) } -func BatchRecordConsume(ctx context.Context, group string, code int, channelID int, promptTokens int, completionTokens int, modelName string, tokenID int, tokenName string, amount float64, price float64, completionPrice float64, endpoint string, content string) error { +func BatchRecordConsume( + requestID string, + requestAt time.Time, + group string, + code int, + channelID int, + promptTokens int, + completionTokens int, + modelName string, + tokenID int, + tokenName string, + amount float64, + price float64, + completionPrice float64, + endpoint string, + content string, + mode int, + requestDetail *RequestDetail, +) error { errs := []error{} - err := RecordConsumeLog(ctx, group, code, channelID, promptTokens, completionTokens, modelName, tokenID, tokenName, amount, price, completionPrice, endpoint, content) + err := RecordConsumeLog( + requestID, + requestAt, + group, + code, + channelID, + promptTokens, + completionTokens, + modelName, + tokenID, + tokenName, + amount, + price, + completionPrice, + endpoint, + content, + mode, + requestDetail, + ) if err != nil { errs = append(errs, fmt.Errorf("failed to record log: %w", err)) } diff --git a/service/aiproxy/monitor/manage.go b/service/aiproxy/monitor/manage.go deleted file mode 100644 index 15d58110057..00000000000 --- a/service/aiproxy/monitor/manage.go +++ /dev/null @@ -1,55 +0,0 @@ -package monitor - -import ( - "net/http" - "strings" - - "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -func ShouldDisableChannel(err *model.Error, statusCode int) bool { - if !config.GetAutomaticDisableChannelEnabled() { - return false - } - if err == nil { - return false - } - if statusCode == http.StatusUnauthorized { - return true - } - switch err.Type { - case "insufficient_quota", "authentication_error", "permission_error", "forbidden": - return true - } - if err.Code == "invalid_api_key" || err.Code == "account_deactivated" { - return true - } - - lowerMessage := strings.ToLower(err.Message) - if strings.Contains(lowerMessage, "your access was terminated") || - strings.Contains(lowerMessage, "violation of our policies") || - strings.Contains(lowerMessage, "your credit balance is too low") || - strings.Contains(lowerMessage, "organization has been disabled") || - strings.Contains(lowerMessage, "credit") || - strings.Contains(lowerMessage, "balance") || - strings.Contains(lowerMessage, "permission denied") || - strings.Contains(lowerMessage, "organization has been restricted") || // groq - strings.Contains(lowerMessage, "已欠费") { - return true - } - return false -} - -func ShouldEnableChannel(err error, openAIErr *model.Error) bool { - if !config.GetAutomaticEnableChannelWhenTestSucceedEnabled() { - return false - } - if err != nil { - return false - } - if openAIErr != nil { - return false - } - return true -} diff --git a/service/aiproxy/monitor/metric.go b/service/aiproxy/monitor/metric.go deleted file mode 100644 index bd7b9914606..00000000000 --- a/service/aiproxy/monitor/metric.go +++ /dev/null @@ -1,76 +0,0 @@ -package monitor - -import ( - "github.com/labring/sealos/service/aiproxy/common/config" - "github.com/labring/sealos/service/aiproxy/model" -) - -var ( - store = make(map[int][]bool) - metricSuccessChan = make(chan int, config.MetricSuccessChanSize) - metricFailChan = make(chan int, config.MetricFailChanSize) -) - -func consumeSuccess(channelID int) { - if len(store[channelID]) > config.MetricQueueSize { - store[channelID] = store[channelID][1:] - } - store[channelID] = append(store[channelID], true) -} - -func consumeFail(channelID int) (bool, float64) { - if len(store[channelID]) > config.MetricQueueSize { - store[channelID] = store[channelID][1:] - } - store[channelID] = append(store[channelID], false) - successCount := 0 - for _, success := range store[channelID] { - if success { - successCount++ - } - } - successRate := float64(successCount) / float64(len(store[channelID])) - if len(store[channelID]) < config.MetricQueueSize { - return false, successRate - } - if successRate < config.MetricSuccessRateThreshold { - store[channelID] = make([]bool, 0) - return true, successRate - } - return false, successRate -} - -func metricSuccessConsumer() { - for channelID := range metricSuccessChan { - consumeSuccess(channelID) - } -} - -func metricFailConsumer() { - for channelID := range metricFailChan { - disable, _ := consumeFail(channelID) - if disable { - _ = model.DisableChannelByID(channelID) - } - } -} - -func init() { - if config.EnableMetric { - go metricSuccessConsumer() - go metricFailConsumer() - } -} - -func Emit(channelID int, success bool) { - if !config.EnableMetric { - return - } - go func() { - if success { - metricSuccessChan <- channelID - } else { - metricFailChan <- channelID - } - }() -} diff --git a/service/aiproxy/relay/adaptor.go b/service/aiproxy/relay/adaptor.go deleted file mode 100644 index 11669d62d06..00000000000 --- a/service/aiproxy/relay/adaptor.go +++ /dev/null @@ -1,63 +0,0 @@ -package relay - -import ( - "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/aiproxy" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/ali" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/anthropic" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/baidu" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/cloudflare" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/cohere" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/coze" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/deepl" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/gemini" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/ollama" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/palm" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/tencent" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/vertexai" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/xunfei" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/zhipu" - "github.com/labring/sealos/service/aiproxy/relay/apitype" -) - -func GetAdaptor(apiType int) adaptor.Adaptor { - switch apiType { - case apitype.AIProxyLibrary: - return &aiproxy.Adaptor{} - case apitype.Ali: - return &ali.Adaptor{} - case apitype.Anthropic: - return &anthropic.Adaptor{} - case apitype.AwsClaude: - return &aws.Adaptor{} - case apitype.Baidu: - return &baidu.Adaptor{} - case apitype.Gemini: - return &gemini.Adaptor{} - case apitype.OpenAI: - return &openai.Adaptor{} - case apitype.PaLM: - return &palm.Adaptor{} - case apitype.Tencent: - return &tencent.Adaptor{} - case apitype.Xunfei: - return &xunfei.Adaptor{} - case apitype.Zhipu: - return &zhipu.Adaptor{} - case apitype.Ollama: - return &ollama.Adaptor{} - case apitype.Coze: - return &coze.Adaptor{} - case apitype.Cohere: - return &cohere.Adaptor{} - case apitype.Cloudflare: - return &cloudflare.Adaptor{} - case apitype.DeepL: - return &deepl.Adaptor{} - case apitype.VertexAI: - return &vertexai.Adaptor{} - } - return nil -} diff --git a/service/aiproxy/relay/adaptor/ai360/adaptor.go b/service/aiproxy/relay/adaptor/ai360/adaptor.go new file mode 100644 index 00000000000..e53cc50b10a --- /dev/null +++ b/service/aiproxy/relay/adaptor/ai360/adaptor.go @@ -0,0 +1,28 @@ +package ai360 + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://ai.360.cn" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "ai360" +} diff --git a/service/aiproxy/relay/adaptor/ai360/constants.go b/service/aiproxy/relay/adaptor/ai360/constants.go index cfc3cb2833f..6160a450c16 100644 --- a/service/aiproxy/relay/adaptor/ai360/constants.go +++ b/service/aiproxy/relay/adaptor/ai360/constants.go @@ -1,8 +1,29 @@ package ai360 -var ModelList = []string{ - "360GPT_S2_V9", - "embedding-bert-512-v1", - "embedding_s1_v1", - "semantic_similarity_s1_v1", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "360GPT_S2_V9", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAI360, + }, + { + Model: "embedding-bert-512-v1", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerAI360, + }, + { + Model: "embedding_s1_v1", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerAI360, + }, + { + Model: "semantic_similarity_s1_v1", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerAI360, + }, } diff --git a/service/aiproxy/relay/adaptor/aiproxy/adaptor.go b/service/aiproxy/relay/adaptor/aiproxy/adaptor.go deleted file mode 100644 index dcc15255878..00000000000 --- a/service/aiproxy/relay/adaptor/aiproxy/adaptor.go +++ /dev/null @@ -1,75 +0,0 @@ -package aiproxy - -import ( - "errors" - "io" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -type Adaptor struct { - meta *meta.Meta -} - -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} - -func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/api/library/ask", nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - aiProxyLibraryRequest := ConvertRequest(request) - aiProxyLibraryRequest.LibraryID = a.meta.Config.LibraryID - return aiProxyLibraryRequest, nil -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp) - } else { - err, usage = Handler(c, resp) - } - return -} - -func (a *Adaptor) GetModelList() []string { - return ModelList -} - -func (a *Adaptor) GetChannelName() string { - return "aiproxy" -} diff --git a/service/aiproxy/relay/adaptor/aiproxy/constants.go b/service/aiproxy/relay/adaptor/aiproxy/constants.go deleted file mode 100644 index 1bdad8b1711..00000000000 --- a/service/aiproxy/relay/adaptor/aiproxy/constants.go +++ /dev/null @@ -1,9 +0,0 @@ -package aiproxy - -import "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - -var ModelList = []string{""} - -func init() { - ModelList = openai.ModelList -} diff --git a/service/aiproxy/relay/adaptor/aiproxy/main.go b/service/aiproxy/relay/adaptor/aiproxy/main.go deleted file mode 100644 index a06b0ab10b0..00000000000 --- a/service/aiproxy/relay/adaptor/aiproxy/main.go +++ /dev/null @@ -1,185 +0,0 @@ -package aiproxy - -import ( - "bufio" - "fmt" - "net/http" - "slices" - "strconv" - - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/render" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/common/random" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/constant" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -// https://docs.aiproxy.io/dev/library#使用已经定制好的知识库进行对话问答 - -func ConvertRequest(request *model.GeneralOpenAIRequest) *LibraryRequest { - query := "" - if len(request.Messages) != 0 { - query = request.Messages[len(request.Messages)-1].StringContent() - } - return &LibraryRequest{ - Model: request.Model, - Stream: request.Stream, - Query: query, - } -} - -func aiProxyDocuments2Markdown(documents []LibraryDocument) string { - if len(documents) == 0 { - return "" - } - content := "\n\n参考文档:\n" - for i, document := range documents { - content += fmt.Sprintf("%d. [%s](%s)\n", i+1, document.Title, document.URL) - } - return content -} - -func responseAIProxyLibrary2OpenAI(response *LibraryResponse) *openai.TextResponse { - content := response.Answer + aiProxyDocuments2Markdown(response.Documents) - choice := openai.TextResponseChoice{ - Index: 0, - Message: model.Message{ - Role: "assistant", - Content: content, - }, - FinishReason: "stop", - } - fullTextResponse := openai.TextResponse{ - ID: "chatcmpl-" + random.GetUUID(), - Object: "chat.completion", - Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, - } - return &fullTextResponse -} - -func documentsAIProxyLibrary(documents []LibraryDocument) *openai.ChatCompletionsStreamResponse { - var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = aiProxyDocuments2Markdown(documents) - choice.FinishReason = &constant.StopFinishReason - return &openai.ChatCompletionsStreamResponse{ - ID: "chatcmpl-" + random.GetUUID(), - Object: "chat.completion.chunk", - Created: helper.GetTimestamp(), - Model: "", - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, - } -} - -func streamResponseAIProxyLibrary2OpenAI(response *LibraryStreamResponse) *openai.ChatCompletionsStreamResponse { - var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = response.Content - return &openai.ChatCompletionsStreamResponse{ - ID: "chatcmpl-" + random.GetUUID(), - Object: "chat.completion.chunk", - Created: helper.GetTimestamp(), - Model: response.Model, - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, - } -} - -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var usage model.Usage - var documents []LibraryDocument - scanner := bufio.NewScanner(resp.Body) - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - if i := slices.Index(data, '\n'); i >= 0 { - return i + 1, data[0:i], nil - } - if atEOF { - return len(data), data, nil - } - return 0, nil, nil - }) - - common.SetEventStreamHeaders(c) - - for scanner.Scan() { - data := scanner.Bytes() - if len(data) < 6 || conv.BytesToString(data[:6]) != "data: " { - continue - } - data = data[6:] - - if conv.BytesToString(data) == "[DONE]" { - break - } - - var AIProxyLibraryResponse LibraryStreamResponse - err := json.Unmarshal(data, &AIProxyLibraryResponse) - if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) - continue - } - if len(AIProxyLibraryResponse.Documents) != 0 { - documents = AIProxyLibraryResponse.Documents - } - response := streamResponseAIProxyLibrary2OpenAI(&AIProxyLibraryResponse) - err = render.ObjectData(c, response) - if err != nil { - logger.SysError(err.Error()) - } - } - - if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) - } - - response := documentsAIProxyLibrary(documents) - err := render.ObjectData(c, response) - if err != nil { - logger.SysError(err.Error()) - } - render.Done(c) - - return nil, &usage -} - -func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var AIProxyLibraryResponse LibraryResponse - err := json.NewDecoder(resp.Body).Decode(&AIProxyLibraryResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - if AIProxyLibraryResponse.ErrCode != 0 { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: AIProxyLibraryResponse.Message, - Type: strconv.Itoa(AIProxyLibraryResponse.ErrCode), - Code: AIProxyLibraryResponse.ErrCode, - }, - StatusCode: resp.StatusCode, - }, nil - } - fullTextResponse := responseAIProxyLibrary2OpenAI(&AIProxyLibraryResponse) - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, err = c.Writer.Write(jsonResponse) - if err != nil { - return openai.ErrorWrapper(err, "write_response_body_failed", http.StatusInternalServerError), nil - } - return nil, &fullTextResponse.Usage -} diff --git a/service/aiproxy/relay/adaptor/aiproxy/model.go b/service/aiproxy/relay/adaptor/aiproxy/model.go deleted file mode 100644 index 4030e5fbc15..00000000000 --- a/service/aiproxy/relay/adaptor/aiproxy/model.go +++ /dev/null @@ -1,32 +0,0 @@ -package aiproxy - -type LibraryRequest struct { - Model string `json:"model"` - Query string `json:"query"` - LibraryID string `json:"libraryId"` - Stream bool `json:"stream"` -} - -type LibraryError struct { - Message string `json:"message"` - ErrCode int `json:"errCode"` -} - -type LibraryDocument struct { - Title string `json:"title"` - URL string `json:"url"` -} - -type LibraryResponse struct { - LibraryError - Answer string `json:"answer"` - Documents []LibraryDocument `json:"documents"` - Success bool `json:"success"` -} - -type LibraryStreamResponse struct { - Content string `json:"content"` - Model string `json:"model"` - Documents []LibraryDocument `json:"documents"` - Finish bool `json:"finish"` -} diff --git a/service/aiproxy/relay/adaptor/ali/adaptor.go b/service/aiproxy/relay/adaptor/ali/adaptor.go index 40de93dc11e..86b23fb67f6 100644 --- a/service/aiproxy/relay/adaptor/ali/adaptor.go +++ b/service/aiproxy/relay/adaptor/ali/adaptor.go @@ -1,108 +1,119 @@ package ali import ( + "bytes" "errors" "io" "net/http" + "strings" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) // https://help.aliyun.com/zh/dashscope/developer-reference/api-details -type Adaptor struct { - meta *meta.Meta -} +type Adaptor struct{} -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} +const baseURL = "https://dashscope.aliyuncs.com" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } switch meta.Mode { case relaymode.Embeddings: - return meta.BaseURL + "/api/v1/services/embeddings/text-embedding/text-embedding", nil + return u + "/api/v1/services/embeddings/text-embedding/text-embedding", nil case relaymode.ImagesGenerations: - return meta.BaseURL + "/api/v1/services/aigc/text2image/image-synthesis", nil + return u + "/api/v1/services/aigc/text2image/image-synthesis", nil + case relaymode.ChatCompletions: + return u + "/compatible-mode/v1/chat/completions", nil + case relaymode.AudioSpeech, relaymode.AudioTranscription: + return u + "/api-ws/v1/inference", nil + case relaymode.Rerank: + return u + "/api/v1/services/rerank/text-rerank/text-rerank", nil default: - return meta.BaseURL + "/compatible-mode/v1/chat/completions", nil + return "", errors.New("unsupported mode") } } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - if meta.IsStream { - req.Header.Set("Accept", "text/event-stream") - req.Header.Set("X-Dashscope-Sse", "enable") - } - req.Header.Set("Authorization", "Bearer "+meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+meta.Channel.Key) - if meta.Mode == relaymode.ImagesGenerations { - req.Header.Set("X-Dashscope-Async", "enable") - } - if a.meta.Config.Plugin != "" { - req.Header.Set("X-Dashscope-Plugin", a.meta.Config.Plugin) + if meta.Channel.Config.Plugin != "" { + req.Header.Set("X-Dashscope-Plugin", meta.Channel.Config.Plugin) } return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - switch relayMode { +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + switch meta.Mode { + case relaymode.ImagesGenerations: + return ConvertImageRequest(meta, req) + case relaymode.Rerank: + return ConvertRerankRequest(meta, req) case relaymode.Embeddings: - aliEmbeddingRequest := ConvertEmbeddingRequest(request) - return aliEmbeddingRequest, nil + return ConvertEmbeddingsRequest(meta, req) + case relaymode.ChatCompletions: + return openai.ConvertRequest(meta, req) + case relaymode.AudioSpeech: + return ConvertTTSRequest(meta, req) + case relaymode.AudioTranscription: + return ConvertSTTRequest(meta, req) default: - aliRequest := ConvertRequest(request) - return aliRequest, nil + return nil, nil, errors.New("unsupported convert request mode") } } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func (a *Adaptor) DoRequest(meta *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + switch meta.Mode { + case relaymode.AudioSpeech: + return TTSDoRequest(meta, req) + case relaymode.AudioTranscription: + return STTDoRequest(meta, req) + case relaymode.ChatCompletions: + if meta.IsChannelTest && strings.Contains(meta.ActualModelName, "-ocr") { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(nil)), + }, nil + } + fallthrough + default: + return utils.DoRequest(req) } - - aliRequest := ConvertImageRequest(*request) - return aliRequest, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) } -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, _, usage = openai.StreamHandler(c, resp, meta.Mode) - } else { - switch meta.Mode { - case relaymode.Embeddings: - err, usage = EmbeddingHandler(c, resp) - case relaymode.ImagesGenerations: - err, usage = ImageHandler(c, resp, meta.APIKey) - default: - err, usage = openai.Handler(c, resp, meta.PromptTokens, meta.ActualModelName) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.Embeddings: + usage, err = EmbeddingsHandler(meta, c, resp) + case relaymode.ImagesGenerations: + usage, err = ImageHandler(meta, c, resp) + case relaymode.ChatCompletions: + if meta.IsChannelTest && strings.Contains(meta.ActualModelName, "-ocr") { + return nil, nil } + usage, err = openai.DoResponse(meta, c, resp) + case relaymode.Rerank: + usage, err = RerankHandler(meta, c, resp) + case relaymode.AudioSpeech: + usage, err = TTSDoResponse(meta, c, resp) + case relaymode.AudioTranscription: + usage, err = STTDoResponse(meta, c, resp) + default: + return nil, openai.ErrorWrapperWithMessage("unsupported response mode", "unsupported_mode", http.StatusBadRequest) } return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/ali/constants.go b/service/aiproxy/relay/adaptor/ali/constants.go index 3f24ce2e141..cd4629c5722 100644 --- a/service/aiproxy/relay/adaptor/ali/constants.go +++ b/service/aiproxy/relay/adaptor/ali/constants.go @@ -1,7 +1,677 @@ package ali -var ModelList = []string{ - "qwen-turbo", "qwen-plus", "qwen-max", "qwen-max-longcontext", - "text-embedding-v1", - "ali-stable-diffusion-xl", "ali-stable-diffusion-v1.5", "wanx-v1", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +// https://help.aliyun.com/zh/model-studio/getting-started/models?spm=a2c4g.11186623.0.i12#ced16cb6cdfsy + +var ModelList = []*model.ModelConfig{ + // 通义千问-Max + { + Model: "qwen-max", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.02, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32768, + model.ModelConfigMaxInputTokensKey: 30720, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen-max-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.02, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32768, + model.ModelConfigMaxInputTokensKey: 30720, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + + // 通义千问-Plus + { + Model: "qwen-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0008, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen-plus-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0008, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 8000, + }, + }, + + // 通义千问-Turbo + { + Model: "qwen-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen-turbo-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 1000000, + model.ModelConfigMaxInputTokensKey: 1000000, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + + // Qwen-Long + { + Model: "qwen-long", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0005, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 10000000, + model.ModelConfigMaxInputTokensKey: 10000000, + model.ModelConfigMaxOutputTokensKey: 6000, + }, + }, + + // 通义千问VL + { + Model: "qwen-vl-max", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.02, + OutputPrice: 0.02, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + { + Model: "qwen-vl-max-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.02, + OutputPrice: 0.02, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + { + Model: "qwen-vl-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.008, + OutputPrice: 0.008, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8000, + model.ModelConfigMaxInputTokensKey: 6000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + { + Model: "qwen-vl-plus-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.008, + OutputPrice: 0.008, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + + // 通义千问OCR + { + Model: "qwen-vl-ocr", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.005, + OutputPrice: 0.005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 34096, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "qwen-vl-ocr-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.005, + OutputPrice: 0.005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 34096, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + // 通义千问Math + { + Model: "qwen-math-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.004, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + { + Model: "qwen-math-plus-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.004, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + { + Model: "qwen-math-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + { + Model: "qwen-math-turbo-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + + // 通义千问Coder + { + Model: "qwen-coder-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0035, + OutputPrice: 0.007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen-coder-plus-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0035, + OutputPrice: 0.007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen-coder-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen-coder-turbo-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + + // 通义千问2.5 + { + Model: "qwen2.5-72b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.004, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen2.5-32b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0035, + OutputPrice: 0.007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen2.5-14b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen2.5-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + + // 通义千问2 + { + Model: "qwen2-72b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.004, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 6144, + }, + }, + { + Model: "qwen2-57b-a14b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0035, + OutputPrice: 0.007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 65536, + model.ModelConfigMaxInputTokensKey: 63488, + model.ModelConfigMaxOutputTokensKey: 6144, + }, + }, + { + Model: "qwen2-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 6144, + }, + }, + + // 通义千问1.5 + { + Model: "qwen1.5-110b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.007, + OutputPrice: 0.014, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 8000, + }, + }, + { + Model: "qwen1.5-72b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.005, + OutputPrice: 0.01, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 8000, + }, + }, + { + Model: "qwen1.5-32b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0035, + OutputPrice: 0.007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 8000, + }, + }, + { + Model: "qwen1.5-14b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.004, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8000, + model.ModelConfigMaxInputTokensKey: 6000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + { + Model: "qwen1.5-7b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8000, + model.ModelConfigMaxInputTokensKey: 6000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + + // 通义千问 + { + Model: "qwen-72b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.02, + OutputPrice: 0.02, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + model.ModelConfigMaxInputTokensKey: 30000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + { + Model: "qwen-14b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.008, + OutputPrice: 0.008, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8000, + model.ModelConfigMaxInputTokensKey: 6000, + model.ModelConfigMaxOutputTokensKey: 2000, + }, + }, + { + Model: "qwen-7b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.006, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 7500, + model.ModelConfigMaxInputTokensKey: 6000, + model.ModelConfigMaxOutputTokensKey: 1500, + }, + }, + + // 通义千问数学模型 + { + Model: "qwen2.5-math-72b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.004, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + { + Model: "qwen2.5-math-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + { + Model: "qwen2-math-72b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.004, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + { + Model: "qwen2-math-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxInputTokensKey: 3072, + model.ModelConfigMaxOutputTokensKey: 3072, + }, + }, + + // 通义千问Coder + { + Model: "qwen2.5-coder-32b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0035, + OutputPrice: 0.007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen2.5-coder-14b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.002, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + { + Model: "qwen2.5-coder-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 131072, + model.ModelConfigMaxInputTokensKey: 129024, + model.ModelConfigMaxOutputTokensKey: 8192, + }, + }, + + // stable-diffusion + { + Model: "stable-diffusion-xl", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + }, + { + Model: "stable-diffusion-v1.5", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + }, + { + Model: "stable-diffusion-3.5-large", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + }, + { + Model: "stable-diffusion-3.5-large-turbo", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + }, + { + Model: "sambert-v1", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.1, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 10000, + model.ModelConfigSupportFormatsKey: []string{"mp3", "wav", "pcm"}, + model.ModelConfigSupportVoicesKey: []string{ + "zhinan", + "zhiqi", + "zhichu", + "zhide", + "zhijia", + "zhiru", + "zhiqian", + "zhixiang", + "zhiwei", + "zhihao", + "zhijing", + "zhiming", + "zhimo", + "zhina", + "zhishu", + "zhistella", + "zhiting", + "zhixiao", + "zhiya", + "zhiye", + "zhiying", + "zhiyuan", + "zhiyue", + "zhigui", + "zhishuo", + "zhimiao-emo", + "zhimao", + "zhilun", + "zhifei", + "zhida", + "indah", + "clara", + "hanna", + "beth", + "betty", + "cally", + "cindy", + "eva", + "donna", + "brian", + "waan", + }, + }, + }, + + { + Model: "paraformer-realtime-v2", + Type: relaymode.AudioTranscription, + Owner: model.ModelOwnerAlibaba, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 10000, + model.ModelConfigSupportFormatsKey: []string{"pcm", "wav", "opus", "speex", "aac", "amr"}, + }, + }, + + { + Model: "gte-rerank", + Type: relaymode.Rerank, + Owner: model.ModelOwnerAlibaba, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4000, + model.ModelConfigMaxInputTokensKey: 4000, + }, + }, + + { + Model: "text-embedding-v1", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 2048, + }, + }, + { + Model: "text-embedding-v2", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 2048, + }, + }, + { + Model: "text-embedding-v3", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerAlibaba, + InputPrice: 0.0007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 8192, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/ali/embeddings.go b/service/aiproxy/relay/adaptor/ali/embeddings.go new file mode 100644 index 00000000000..d460b87bc25 --- /dev/null +++ b/service/aiproxy/relay/adaptor/ali/embeddings.go @@ -0,0 +1,100 @@ +package ali + +import ( + "bytes" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +func ConvertEmbeddingsRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + var reqMap map[string]any + err := common.UnmarshalBodyReusable(req, &reqMap) + if err != nil { + return nil, nil, err + } + reqMap["model"] = meta.ActualModelName + input, ok := reqMap["input"] + if !ok { + return nil, nil, errors.New("input is required") + } + switch v := input.(type) { + case string: + reqMap["input"] = map[string]any{ + "texts": []string{v}, + } + case []any: + reqMap["input"] = map[string]any{ + "texts": v, + } + } + parameters := make(map[string]any) + for k, v := range reqMap { + if k == "model" || k == "input" { + continue + } + parameters[k] = v + delete(reqMap, k) + } + reqMap["parameters"] = parameters + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(jsonData), nil +} + +func embeddingResponse2OpenAI(meta *meta.Meta, response *EmbeddingResponse) *openai.EmbeddingResponse { + openAIEmbeddingResponse := openai.EmbeddingResponse{ + Object: "list", + Data: make([]*openai.EmbeddingResponseItem, 0, 1), + Model: meta.OriginModelName, + Usage: response.Usage, + } + + for i, embedding := range response.Output.Embeddings { + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, &openai.EmbeddingResponseItem{ + Object: `embedding`, + Index: i, + Embedding: embedding.Embedding, + }) + } + return &openAIEmbeddingResponse +} + +func EmbeddingsHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, openai.ErrorWrapper(err, "read_response_body_failed", resp.StatusCode) + } + var respBody EmbeddingResponse + err = json.Unmarshal(responseBody, &respBody) + if err != nil { + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", resp.StatusCode) + } + if respBody.Usage.PromptTokens == 0 { + respBody.Usage.PromptTokens = respBody.Usage.TotalTokens + } + openaiResponse := embeddingResponse2OpenAI(meta, &respBody) + data, err := json.Marshal(openaiResponse) + if err != nil { + return &respBody.Usage, openai.ErrorWrapper(err, "marshal_response_body_failed", resp.StatusCode) + } + _, err = c.Writer.Write(data) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return &openaiResponse.Usage, nil +} diff --git a/service/aiproxy/relay/adaptor/ali/image.go b/service/aiproxy/relay/adaptor/ali/image.go index d6b01a3b40a..92c7d048a87 100644 --- a/service/aiproxy/relay/adaptor/ali/image.go +++ b/service/aiproxy/relay/adaptor/ali/image.go @@ -1,50 +1,84 @@ package ali import ( + "bytes" "context" - "encoding/base64" "errors" "io" "net/http" + "strings" "time" "github.com/gin-gonic/gin" json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/common/image" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" + log "github.com/sirupsen/logrus" ) -func ImageHandler(c *gin.Context, resp *http.Response, apiKey string) (*model.ErrorWithStatusCode, *model.Usage) { - responseFormat := c.GetString("response_format") +const MetaResponseFormat = "response_format" + +func ConvertImageRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalImageRequest(req) + if err != nil { + return nil, nil, err + } + request.Model = meta.ActualModelName + + var imageRequest ImageRequest + imageRequest.Input.Prompt = request.Prompt + imageRequest.Model = request.Model + imageRequest.Parameters.Size = strings.ReplaceAll(request.Size, "x", "*") + imageRequest.Parameters.N = request.N + imageRequest.ResponseFormat = request.ResponseFormat + + meta.Set(MetaResponseFormat, request.ResponseFormat) + + data, err := json.Marshal(&imageRequest) + if err != nil { + return nil, nil, err + } + return http.Header{ + "X-Dashscope-Async": {"enable"}, + }, bytes.NewReader(data), nil +} + +func ImageHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + log := middleware.GetLogger(c) + + responseFormat := meta.MustGet(MetaResponseFormat).(string) var aliTaskResponse TaskResponse responseBody, err := io.ReadAll(resp.Body) if err != nil { - return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) } err = resp.Body.Close() if err != nil { - return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) } err = json.Unmarshal(responseBody, &aliTaskResponse) if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } if aliTaskResponse.Message != "" { - logger.SysErrorf("aliAsyncTask err: %s", responseBody) - return openai.ErrorWrapper(errors.New(aliTaskResponse.Message), "ali_async_task_failed", http.StatusInternalServerError), nil + log.Error("aliAsyncTask err: " + aliTaskResponse.Message) + return nil, openai.ErrorWrapper(errors.New(aliTaskResponse.Message), "ali_async_task_failed", http.StatusInternalServerError) } - aliResponse, err := asyncTaskWait(c, aliTaskResponse.Output.TaskID, apiKey) + aliResponse, err := asyncTaskWait(c, aliTaskResponse.Output.TaskID, meta.Channel.Key) if err != nil { - return openai.ErrorWrapper(err, "ali_async_task_wait_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "ali_async_task_wait_failed", http.StatusInternalServerError) } if aliResponse.Output.TaskStatus != "SUCCEEDED" { - return &model.ErrorWithStatusCode{ + return nil, &model.ErrorWithStatusCode{ Error: model.Error{ Message: aliResponse.Output.Message, Type: "ali_error", @@ -52,18 +86,21 @@ func ImageHandler(c *gin.Context, resp *http.Response, apiKey string) (*model.Er Code: aliResponse.Output.Code, }, StatusCode: resp.StatusCode, - }, nil + } } - fullTextResponse := responseAli2OpenAIImage(aliResponse, responseFormat) + fullTextResponse := responseAli2OpenAIImage(c.Request.Context(), aliResponse, responseFormat) jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) } c.Writer.Header().Set("Content-Type", "application/json") c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, nil + _, err = c.Writer.Write(jsonResponse) + if err != nil { + log.Error("aliImageHandler write response body failed: " + err.Error()) + } + return &model.Usage{}, nil } func asyncTask(ctx context.Context, taskID string, key string) (*TaskResponse, error) { @@ -81,7 +118,6 @@ func asyncTask(ctx context.Context, taskID string, key string) (*TaskResponse, e client := &http.Client{} resp, err := client.Do(req) if err != nil { - logger.SysError("aliAsyncTask client.Do err: " + err.Error()) return &aliResponse, err } defer resp.Body.Close() @@ -89,7 +125,6 @@ func asyncTask(ctx context.Context, taskID string, key string) (*TaskResponse, e var response TaskResponse err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { - logger.SysError("aliAsyncTask NewDecoder err: " + err.Error()) return &aliResponse, err } @@ -131,7 +166,7 @@ func asyncTaskWait(ctx context.Context, taskID string, key string) (*TaskRespons return nil, errors.New("aliAsyncTaskWait timeout") } -func responseAli2OpenAIImage(response *TaskResponse, responseFormat string) *openai.ImageResponse { +func responseAli2OpenAIImage(ctx context.Context, response *TaskResponse, responseFormat string) *openai.ImageResponse { imageResponse := openai.ImageResponse{ Created: helper.GetTimestamp(), } @@ -140,21 +175,21 @@ func responseAli2OpenAIImage(response *TaskResponse, responseFormat string) *ope var b64Json string if responseFormat == "b64_json" { // 读取 data.Url 的图片数据并转存到 b64Json - imageData, err := getImageData(data.URL) + _, imageData, err := image.GetImageFromURL(ctx, data.URL) if err != nil { // 处理获取图片数据失败的情况 - logger.SysError("getImageData Error getting image data: " + err.Error()) + log.Error("getImageData Error getting image data: " + err.Error()) continue } // 将图片数据转为 Base64 编码的字符串 - b64Json = Base64Encode(imageData) + b64Json = imageData } else { // 如果 responseFormat 不是 "b64_json",则直接使用 data.B64Image b64Json = data.B64Image } - imageResponse.Data = append(imageResponse.Data, openai.ImageData{ + imageResponse.Data = append(imageResponse.Data, &openai.ImageData{ URL: data.URL, B64Json: b64Json, RevisedPrompt: "", @@ -162,27 +197,3 @@ func responseAli2OpenAIImage(response *TaskResponse, responseFormat string) *ope } return &imageResponse } - -func getImageData(url string) ([]byte, error) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) - if err != nil { - return nil, err - } - response, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer response.Body.Close() - - imageData, err := io.ReadAll(response.Body) - if err != nil { - return nil, err - } - - return imageData, nil -} - -func Base64Encode(data []byte) string { - b64Json := base64.StdEncoding.EncodeToString(data) - return b64Json -} diff --git a/service/aiproxy/relay/adaptor/ali/main.go b/service/aiproxy/relay/adaptor/ali/main.go deleted file mode 100644 index 83ae8bea420..00000000000 --- a/service/aiproxy/relay/adaptor/ali/main.go +++ /dev/null @@ -1,106 +0,0 @@ -package ali - -import ( - "net/http" - "strings" - - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r - -const EnableSearchModelSuffix = "-internet" - -func ConvertRequest(request *model.GeneralOpenAIRequest) *model.GeneralOpenAIRequest { - if request.TopP != nil && *request.TopP >= 1 { - *request.TopP = 0.9999 - } - if request.Stream { - if request.StreamOptions == nil { - request.StreamOptions = &model.StreamOptions{} - } - request.StreamOptions.IncludeUsage = true - } - return request -} - -func ConvertEmbeddingRequest(request *model.GeneralOpenAIRequest) *EmbeddingRequest { - return &EmbeddingRequest{ - Model: request.Model, - Input: struct { - Texts []string `json:"texts"` - }{ - Texts: request.ParseInput(), - }, - } -} - -func ConvertImageRequest(request model.ImageRequest) *ImageRequest { - var imageRequest ImageRequest - imageRequest.Input.Prompt = request.Prompt - imageRequest.Model = request.Model - imageRequest.Parameters.Size = strings.Replace(request.Size, "x", "*", -1) - imageRequest.Parameters.N = request.N - imageRequest.ResponseFormat = request.ResponseFormat - - return &imageRequest -} - -func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - var aliResponse EmbeddingResponse - err := json.NewDecoder(resp.Body).Decode(&aliResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - err = resp.Body.Close() - if err != nil { - return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - - if aliResponse.Code != "" { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: aliResponse.Message, - Type: aliResponse.Code, - Param: aliResponse.RequestID, - Code: aliResponse.Code, - }, - StatusCode: resp.StatusCode, - }, nil - } - requestModel := c.GetString(ctxkey.RequestModel) - fullTextResponse := embeddingResponseAli2OpenAI(&aliResponse) - fullTextResponse.Model = requestModel - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, &fullTextResponse.Usage -} - -func embeddingResponseAli2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { - openAIEmbeddingResponse := openai.EmbeddingResponse{ - Object: "list", - Data: make([]openai.EmbeddingResponseItem, 0, len(response.Output.Embeddings)), - Model: "text-embedding-v1", - Usage: model.Usage{TotalTokens: response.Usage.TotalTokens}, - } - - for _, item := range response.Output.Embeddings { - openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ - Object: `embedding`, - Index: item.TextIndex, - Embedding: item.Embedding, - }) - } - return &openAIEmbeddingResponse -} diff --git a/service/aiproxy/relay/adaptor/ali/model.go b/service/aiproxy/relay/adaptor/ali/model.go index d1f1344670b..c7cc38a689c 100644 --- a/service/aiproxy/relay/adaptor/ali/model.go +++ b/service/aiproxy/relay/adaptor/ali/model.go @@ -1,5 +1,7 @@ package ali +import relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + type ImageRequest struct { Input struct { Prompt string `json:"prompt"` @@ -36,8 +38,8 @@ type TaskResponse struct { Failed int `json:"FAILED,omitempty"` } `json:"task_metrics,omitempty"` } `json:"output,omitempty"` - Usage Usage `json:"usage"` - StatusCode int `json:"status_code,omitempty"` + Usage relaymodel.Usage `json:"usage"` + StatusCode int `json:"status_code,omitempty"` } type EmbeddingRequest struct { @@ -60,7 +62,7 @@ type EmbeddingResponse struct { Output struct { Embeddings []Embedding `json:"embeddings"` } `json:"output"` - Usage Usage `json:"usage"` + Usage relaymodel.Usage `json:"usage"` } type Error struct { @@ -68,9 +70,3 @@ type Error struct { Message string `json:"message"` RequestID string `json:"request_id"` } - -type Usage struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - TotalTokens int `json:"total_tokens"` -} diff --git a/service/aiproxy/relay/adaptor/ali/rerank.go b/service/aiproxy/relay/adaptor/ali/rerank.go new file mode 100644 index 00000000000..4fb4412af00 --- /dev/null +++ b/service/aiproxy/relay/adaptor/ali/rerank.go @@ -0,0 +1,109 @@ +package ali + +import ( + "bytes" + "io" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +type RerankResponse struct { + Usage *RerankUsage `json:"usage"` + RequestID string `json:"request_id"` + Output RerankOutput `json:"output"` +} +type RerankOutput struct { + Results []*relaymodel.RerankResult `json:"results"` +} +type RerankUsage struct { + TotalTokens int `json:"total_tokens"` +} + +func ConvertRerankRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + reqMap := make(map[string]any) + err := common.UnmarshalBodyReusable(req, &reqMap) + if err != nil { + return nil, nil, err + } + reqMap["model"] = meta.ActualModelName + reqMap["input"] = map[string]any{ + "query": reqMap["query"], + "documents": reqMap["documents"], + } + delete(reqMap, "query") + delete(reqMap, "documents") + parameters := make(map[string]any) + for k, v := range reqMap { + if k == "model" || k == "input" { + continue + } + parameters[k] = v + delete(reqMap, k) + } + reqMap["parameters"] = parameters + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(jsonData), nil +} + +func RerankHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + var rerankResponse RerankResponse + err = json.Unmarshal(responseBody, &rerankResponse) + if err != nil { + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + + c.Writer.WriteHeader(resp.StatusCode) + + rerankResp := relaymodel.RerankResponse{ + Meta: relaymodel.RerankMeta{ + Tokens: &relaymodel.RerankMetaTokens{ + InputTokens: rerankResponse.Usage.TotalTokens, + OutputTokens: 0, + }, + }, + Result: rerankResponse.Output.Results, + ID: rerankResponse.RequestID, + } + + var usage *relaymodel.Usage + if rerankResponse.Usage == nil { + usage = &relaymodel.Usage{ + PromptTokens: meta.PromptTokens, + CompletionTokens: 0, + TotalTokens: meta.PromptTokens, + } + } else { + usage = &relaymodel.Usage{ + PromptTokens: rerankResponse.Usage.TotalTokens, + TotalTokens: rerankResponse.Usage.TotalTokens, + } + } + + jsonResponse, err := json.Marshal(&rerankResp) + if err != nil { + return usage, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + _, err = c.Writer.Write(jsonResponse) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return usage, nil +} diff --git a/service/aiproxy/relay/adaptor/ali/stt-realtime.go b/service/aiproxy/relay/adaptor/ali/stt-realtime.go new file mode 100644 index 00000000000..b8bc38e0950 --- /dev/null +++ b/service/aiproxy/relay/adaptor/ali/stt-realtime.go @@ -0,0 +1,204 @@ +package ali + +import ( + "bytes" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/gorilla/websocket" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +type STTMessage struct { + Header STTHeader `json:"header"` + Payload STTPayload `json:"payload"` +} + +type STTHeader struct { + Attributes map[string]any `json:"attributes"` + Action string `json:"action,omitempty"` + TaskID string `json:"task_id"` + Streaming string `json:"streaming,omitempty"` + Event string `json:"event,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +type STTPayload struct { + Model string `json:"model,omitempty"` + TaskGroup string `json:"task_group,omitempty"` + Task string `json:"task,omitempty"` + Function string `json:"function,omitempty"` + Input STTInput `json:"input,omitempty"` + Output STTOutput `json:"output,omitempty"` + Parameters STTParameters `json:"parameters,omitempty"` + Usage STTUsage `json:"usage,omitempty"` +} + +type STTInput struct { + AudioData []byte `json:"audio_data"` +} + +type STTParameters struct { + Format string `json:"format"` + SampleRate int `json:"sample_rate"` +} + +type STTOutput struct { + Text string `json:"text"` +} + +type STTUsage struct { + Characters int `json:"characters"` +} + +func ConvertSTTRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { + err := request.ParseMultipartForm(1024 * 1024 * 4) + if err != nil { + return nil, nil, err + } + + var audioData []byte + if files, ok := request.MultipartForm.File["file"]; !ok { + return nil, nil, errors.New("audio file is required") + } else if len(files) == 1 { + file, err := files[0].Open() + if err != nil { + return nil, nil, err + } + audioData, err = io.ReadAll(file) + file.Close() + if err != nil { + return nil, nil, err + } + } else { + return nil, nil, errors.New("audio file is required") + } + + sttRequest := STTMessage{ + Header: STTHeader{ + Action: "run-task", + Streaming: "duplex", + TaskID: uuid.New().String(), + }, + Payload: STTPayload{ + Model: meta.ActualModelName, + Task: "asr", + TaskGroup: "audio", + Function: "recognition", + Input: STTInput{}, + Parameters: STTParameters{ + Format: "mp3", + SampleRate: 16000, + }, + }, + } + + data, err := json.Marshal(sttRequest) + if err != nil { + return nil, nil, err + } + meta.Set("audio_data", audioData) + meta.Set("task_id", sttRequest.Header.TaskID) + return http.Header{ + "X-DashScope-DataInspection": {"enable"}, + }, bytes.NewReader(data), nil +} + +func STTDoRequest(meta *meta.Meta, req *http.Request) (*http.Response, error) { + wsURL := req.URL + wsURL.Scheme = "wss" + + conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), req.Header) + if err != nil { + return nil, err + } + meta.Set("ws_conn", conn) + + jsonWriter, err := conn.NextWriter(websocket.TextMessage) + if err != nil { + return nil, err + } + defer jsonWriter.Close() + _, err = io.Copy(jsonWriter, req.Body) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(nil), + }, nil +} + +func STTDoResponse(meta *meta.Meta, c *gin.Context, _ *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + log := middleware.GetLogger(c) + + audioData := meta.MustGet("audio_data").([]byte) + taskID := meta.MustGet("task_id").(string) + + conn := meta.MustGet("ws_conn").(*websocket.Conn) + defer conn.Close() + + usage = &relaymodel.Usage{} + + for { + messageType, data, err := conn.ReadMessage() + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_read_msg_failed", "ali_wss_read_msg_failed", http.StatusInternalServerError) + } + + if messageType != websocket.TextMessage { + return usage, openai.ErrorWrapperWithMessage("expect text message, but got binary message", "ali_wss_read_msg_failed", http.StatusInternalServerError) + } + + var msg STTMessage + err = json.Unmarshal(data, &msg) + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_read_msg_failed", "ali_wss_read_msg_failed", http.StatusInternalServerError) + } + switch msg.Header.Event { + case "task-started": + err = conn.WriteMessage(websocket.BinaryMessage, audioData) + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_write_msg_failed", "ali_wss_write_msg_failed", http.StatusInternalServerError) + } + finishMsg := STTMessage{ + Header: STTHeader{ + Action: "finish-task", + TaskID: taskID, + Streaming: "duplex", + }, + Payload: STTPayload{ + Input: STTInput{}, + }, + } + finishData, err := json.Marshal(finishMsg) + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_write_msg_failed", "ali_wss_write_msg_failed", http.StatusInternalServerError) + } + err = conn.WriteMessage(websocket.TextMessage, finishData) + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_write_msg_failed", "ali_wss_write_msg_failed", http.StatusInternalServerError) + } + case "result-generated": + if msg.Payload.Output.Text != "" { + log.Info("STT result: " + msg.Payload.Output.Text) + } + continue + case "task-finished": + usage.PromptTokens = msg.Payload.Usage.Characters + usage.TotalTokens = msg.Payload.Usage.Characters + return usage, nil + case "task-failed": + return usage, openai.ErrorWrapperWithMessage(msg.Header.ErrorMessage, msg.Header.ErrorCode, http.StatusInternalServerError) + } + } +} diff --git a/service/aiproxy/relay/adaptor/ali/tts.go b/service/aiproxy/relay/adaptor/ali/tts.go new file mode 100644 index 00000000000..b1ecf4d31fe --- /dev/null +++ b/service/aiproxy/relay/adaptor/ali/tts.go @@ -0,0 +1,233 @@ +package ali + +import ( + "bytes" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/gorilla/websocket" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" +) + +type TTSMessage struct { + Header TTSHeader `json:"header"` + Payload TTSPayload `json:"payload"` +} + +type TTSHeader struct { + Attributes map[string]any `json:"attributes"` + Action string `json:"action,omitempty"` + TaskID string `json:"task_id"` + Streaming string `json:"streaming,omitempty"` + Event string `json:"event,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +type TTSPayload struct { + Model string `json:"model,omitempty"` + TaskGroup string `json:"task_group,omitempty"` + Task string `json:"task,omitempty"` + Function string `json:"function,omitempty"` + Input TTSInput `json:"input,omitempty"` + Output TTSOutput `json:"output,omitempty"` + Parameters TTSParameters `json:"parameters,omitempty"` + Usage TTSUsage `json:"usage,omitempty"` +} + +type TTSInput struct { + Text string `json:"text"` +} + +type TTSParameters struct { + TextType string `json:"text_type"` + Format string `json:"format"` + SampleRate int `json:"sample_rate,omitempty"` + Volume int `json:"volume"` + Rate float64 `json:"rate"` + Pitch float64 `json:"pitch"` + WordTimestampEnabled bool `json:"word_timestamp_enabled"` + PhonemeTimestampEnabled bool `json:"phoneme_timestamp_enabled"` +} + +type TTSOutput struct { + Sentence TTSSentence `json:"sentence"` +} + +type TTSSentence struct { + Words []TTSWord `json:"words"` + BeginTime int `json:"begin_time"` + EndTime int `json:"end_time"` +} + +type TTSWord struct { + Text string `json:"text"` + Phonemes []TTSPhoneme `json:"phonemes"` + BeginTime int `json:"begin_time"` + EndTime int `json:"end_time"` +} + +type TTSPhoneme struct { + Text string `json:"text"` + BeginTime int `json:"begin_time"` + EndTime int `json:"end_time"` + Tone int `json:"tone"` +} + +type TTSUsage struct { + Characters int `json:"characters"` +} + +var ttsSupportedFormat = map[string]struct{}{ + "pcm": {}, + "wav": {}, + "mp3": {}, +} + +func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalTTSRequest(req) + if err != nil { + return nil, nil, err + } + reqMap, err := utils.UnmarshalMap(req) + if err != nil { + return nil, nil, err + } + var sampleRate int + sampleRateI, ok := reqMap["sample_rate"].(float64) + if ok { + sampleRate = int(sampleRateI) + } + request.Model = meta.ActualModelName + + if strings.HasPrefix(request.Model, "sambert-v") { + voice := request.Voice + if voice == "" { + voice = "zhinan" + } + request.Model = fmt.Sprintf("sambert-%s-v%s", voice, strings.TrimPrefix(request.Model, "sambert-v")) + } + + ttsRequest := TTSMessage{ + Header: TTSHeader{ + Action: "run-task", + Streaming: "out", + TaskID: uuid.New().String(), + }, + Payload: TTSPayload{ + Model: request.Model, + Task: "tts", + TaskGroup: "audio", + Function: "SpeechSynthesizer", + Input: TTSInput{ + Text: request.Input, + }, + Parameters: TTSParameters{ + TextType: "PlainText", + Format: "wav", + Volume: 50, + SampleRate: sampleRate, + Rate: request.Speed, + Pitch: 1.0, + WordTimestampEnabled: true, + PhonemeTimestampEnabled: true, + }, + }, + } + + if _, ok := ttsSupportedFormat[request.ResponseFormat]; ok { + ttsRequest.Payload.Parameters.Format = request.ResponseFormat + } + + if ttsRequest.Payload.Parameters.Rate < 0.5 { + ttsRequest.Payload.Parameters.Rate = 0.5 + } else if ttsRequest.Payload.Parameters.Rate > 2 { + ttsRequest.Payload.Parameters.Rate = 2 + } + + data, err := json.Marshal(ttsRequest) + if err != nil { + return nil, nil, err + } + return http.Header{ + "X-DashScope-DataInspection": {"enable"}, + }, bytes.NewReader(data), nil +} + +func TTSDoRequest(meta *meta.Meta, req *http.Request) (*http.Response, error) { + wsURL := req.URL + wsURL.Scheme = "wss" + + conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), req.Header) + if err != nil { + return nil, err + } + meta.Set("ws_conn", conn) + + writer, err := conn.NextWriter(websocket.TextMessage) + if err != nil { + return nil, err + } + defer writer.Close() + + _, err = io.Copy(writer, req.Body) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(nil), + }, nil +} + +func TTSDoResponse(meta *meta.Meta, c *gin.Context, _ *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + log := middleware.GetLogger(c) + + conn := meta.MustGet("ws_conn").(*websocket.Conn) + defer conn.Close() + + usage = &relaymodel.Usage{} + + for { + messageType, data, err := conn.ReadMessage() + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_read_msg_failed", "ali_wss_read_msg_failed", http.StatusInternalServerError) + } + + var msg TTSMessage + switch messageType { + case websocket.TextMessage: + err = json.Unmarshal(data, &msg) + if err != nil { + return usage, openai.ErrorWrapperWithMessage("ali_wss_read_msg_failed", "ali_wss_read_msg_failed", http.StatusInternalServerError) + } + switch msg.Header.Event { + case "task-started": + continue + case "result-generated": + continue + case "task-finished": + usage.PromptTokens = msg.Payload.Usage.Characters + usage.TotalTokens = msg.Payload.Usage.Characters + return usage, nil + case "task-failed": + return usage, openai.ErrorWrapperWithMessage(msg.Header.ErrorMessage, msg.Header.ErrorCode, http.StatusInternalServerError) + } + case websocket.BinaryMessage: + _, writeErr := c.Writer.Write(data) + if writeErr != nil { + log.Error("write tts response chunk failed: " + writeErr.Error()) + } + } + } +} diff --git a/service/aiproxy/relay/adaptor/anthropic/adaptor.go b/service/aiproxy/relay/adaptor/anthropic/adaptor.go index d3a0b85d0b9..b265b039ccb 100644 --- a/service/aiproxy/relay/adaptor/anthropic/adaptor.go +++ b/service/aiproxy/relay/adaptor/anthropic/adaptor.go @@ -1,29 +1,33 @@ package anthropic import ( - "errors" + "bytes" "io" "net/http" "strings" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) type Adaptor struct{} -func (a *Adaptor) Init(_ *meta.Meta) { -} +const baseURL = "https://api.anthropic.com" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/v1/messages", nil + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + return u + "/v1/messages", nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("X-Api-Key", meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request) error { + req.Header.Set("X-Api-Key", meta.Channel.Key) anthropicVersion := c.Request.Header.Get("Anthropic-Version") if anthropicVersion == "" { anthropicVersion = "2023-06-01" @@ -40,42 +44,33 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *me return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + data, err := ConvertRequest(meta, req) + if err != nil { + return nil, nil, err } - return ConvertRequest(request), nil -} -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") + data2, err := json.Marshal(data) + if err != nil { + return nil, nil, err } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil + return nil, bytes.NewReader(data2), nil } -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + if utils.IsStreamResponse(resp) { + err, usage = StreamHandler(meta, c, resp) } else { - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + err, usage = Handler(meta, c, resp) } return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/anthropic/constants.go b/service/aiproxy/relay/adaptor/anthropic/constants.go index cb574706d48..119e019b40e 100644 --- a/service/aiproxy/relay/adaptor/anthropic/constants.go +++ b/service/aiproxy/relay/adaptor/anthropic/constants.go @@ -1,13 +1,54 @@ package anthropic -var ModelList = []string{ - "claude-instant-1.2", "claude-2.0", "claude-2.1", - "claude-3-haiku-20240307", - "claude-3-5-haiku-20241022", - "claude-3-sonnet-20240229", - "claude-3-opus-20240229", - "claude-3-5-sonnet-20240620", - "claude-3-5-sonnet-20241022", - "claude-3-5-sonnet-latest", - "claude-3-5-haiku-20241022", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "claude-instant-1.2", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-2.0", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-2.1", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-haiku-20240307", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-sonnet-20240229", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-opus-20240229", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-5-sonnet-20240620", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-5-sonnet-20241022", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-5-sonnet-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, } diff --git a/service/aiproxy/relay/adaptor/anthropic/main.go b/service/aiproxy/relay/adaptor/anthropic/main.go index f0efdfa7a7a..00a018455c4 100644 --- a/service/aiproxy/relay/adaptor/anthropic/main.go +++ b/service/aiproxy/relay/adaptor/anthropic/main.go @@ -8,13 +8,14 @@ import ( json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/helper" "github.com/labring/sealos/service/aiproxy/common/image" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" ) @@ -38,7 +39,14 @@ func stopReasonClaude2OpenAI(reason *string) string { } } -func ConvertRequest(textRequest *model.GeneralOpenAIRequest) *Request { +func ConvertRequest(meta *meta.Meta, req *http.Request) (*Request, error) { + var textRequest model.GeneralOpenAIRequest + err := common.UnmarshalBodyReusable(req, &textRequest) + if err != nil { + return nil, err + } + textRequest.Model = meta.ActualModelName + meta.Set("stream", textRequest.Stream) claudeTools := make([]Tool, 0, len(textRequest.Tools)) for _, tool := range textRequest.Tools { @@ -137,7 +145,10 @@ func ConvertRequest(textRequest *model.GeneralOpenAIRequest) *Request { content.Source = &ImageSource{ Type: "base64", } - mimeType, data, _ := image.GetImageFromURL(part.ImageURL.URL) + mimeType, data, err := image.GetImageFromURL(req.Context(), part.ImageURL.URL) + if err != nil { + return nil, err + } content.Source.MediaType = mimeType content.Source.Data = data } @@ -146,7 +157,8 @@ func ConvertRequest(textRequest *model.GeneralOpenAIRequest) *Request { claudeMessage.Content = contents claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) } - return &claudeRequest + + return &claudeRequest, nil } // https://docs.anthropic.com/claude/reference/messages-streaming @@ -154,7 +166,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo var response *Response var responseText string var stopReason string - tools := make([]model.Tool, 0) + tools := make([]*model.Tool, 0) switch claudeResponse.Type { case "message_start": @@ -163,7 +175,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo if claudeResponse.ContentBlock != nil { responseText = claudeResponse.ContentBlock.Text if claudeResponse.ContentBlock.Type == toolUseType { - tools = append(tools, model.Tool{ + tools = append(tools, &model.Tool{ ID: claudeResponse.ContentBlock.ID, Type: "function", Function: model.Function{ @@ -177,7 +189,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo if claudeResponse.Delta != nil { responseText = claudeResponse.Delta.Text if claudeResponse.Delta.Type == "input_json_delta" { - tools = append(tools, model.Tool{ + tools = append(tools, &model.Tool{ Function: model.Function{ Arguments: claudeResponse.Delta.PartialJSON, }, @@ -207,7 +219,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo } var openaiResponse openai.ChatCompletionsStreamResponse openaiResponse.Object = "chat.completion.chunk" - openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + openaiResponse.Choices = []*openai.ChatCompletionsStreamResponseChoice{&choice} return &openaiResponse, response } @@ -216,11 +228,11 @@ func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { if len(claudeResponse.Content) > 0 { responseText = claudeResponse.Content[0].Text } - tools := make([]model.Tool, 0) + tools := make([]*model.Tool, 0) for _, v := range claudeResponse.Content { if v.Type == toolUseType { args, _ := json.Marshal(v.Input) - tools = append(tools, model.Tool{ + tools = append(tools, &model.Tool{ ID: v.ID, Type: "function", // compatible with other OpenAI derivative applications Function: model.Function{ @@ -245,14 +257,16 @@ func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { Model: claudeResponse.Model, Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, + Choices: []*openai.TextResponseChoice{&choice}, } return &fullTextResponse } -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { +func StreamHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { defer resp.Body.Close() + log := middleware.GetLogger(c) + createdTime := helper.GetTimestamp() scanner := bufio.NewScanner(resp.Body) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { @@ -273,7 +287,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var usage model.Usage var modelName string var id string - var lastToolCallChoice openai.ChatCompletionsStreamResponseChoice + var lastToolCallChoice *openai.ChatCompletionsStreamResponseChoice for scanner.Scan() { data := scanner.Bytes() @@ -289,7 +303,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var claudeResponse StreamResponse err := json.Unmarshal(data, &claudeResponse) if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) + log.Error("error unmarshalling stream response: " + err.Error()) continue } @@ -305,7 +319,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC id = "chatcmpl-" + meta.ID continue } - if len(lastToolCallChoice.Delta.ToolCalls) > 0 { + if lastToolCallChoice != nil && len(lastToolCallChoice.Delta.ToolCalls) > 0 { lastArgs := &lastToolCallChoice.Delta.ToolCalls[len(lastToolCallChoice.Delta.ToolCalls)-1].Function if len(lastArgs.Arguments) == 0 { // compatible with OpenAI sending an empty object `{}` when no arguments. lastArgs.Arguments = "{}" @@ -326,12 +340,12 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC } err = render.ObjectData(c, response) if err != nil { - logger.SysError(err.Error()) + log.Error("error rendering stream response: " + err.Error()) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } render.Done(c) @@ -339,7 +353,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC return nil, &usage } -func Handler(c *gin.Context, resp *http.Response, _ int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { +func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { defer resp.Body.Close() var claudeResponse Response @@ -359,7 +373,7 @@ func Handler(c *gin.Context, resp *http.Response, _ int, modelName string) (*mod }, nil } fullTextResponse := ResponseClaude2OpenAI(&claudeResponse) - fullTextResponse.Model = modelName + fullTextResponse.Model = meta.OriginModelName usage := model.Usage{ PromptTokens: claudeResponse.Usage.InputTokens, CompletionTokens: claudeResponse.Usage.OutputTokens, diff --git a/service/aiproxy/relay/adaptor/aws/adaptor.go b/service/aiproxy/relay/adaptor/aws/adaptor.go index 68fa986857d..3295c7dd9d7 100644 --- a/service/aiproxy/relay/adaptor/aws/adaptor.go +++ b/service/aiproxy/relay/adaptor/aws/adaptor.go @@ -5,65 +5,42 @@ import ( "io" "net/http" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor" "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/utils" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) var _ adaptor.Adaptor = new(Adaptor) -type Adaptor struct { - awsAdapter utils.AwsAdapter +type Adaptor struct{} - Meta *meta.Meta - AwsClient *bedrockruntime.Client -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) Init(meta *meta.Meta) { - a.Meta = meta - a.AwsClient = bedrockruntime.New(bedrockruntime.Options{ - Region: meta.Config.Region, - Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(meta.Config.AK, meta.Config.SK, "")), - }) -} - -func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - - adaptor := GetAdaptor(request.Model) +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + adaptor := GetAdaptor(meta.ActualModelName) if adaptor == nil { - return nil, errors.New("adaptor not found") + return nil, nil, errors.New("adaptor not found") } - - a.awsAdapter = adaptor - return adaptor.ConvertRequest(c, relayMode, request) + meta.Set("awsAdapter", adaptor) + return adaptor.ConvertRequest(meta, req) } -func (a *Adaptor) DoResponse(c *gin.Context, _ *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if a.awsAdapter == nil { - return nil, utils.WrapErr(errors.New("awsAdapter is nil")) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, _ *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + adaptor, ok := meta.Get("awsAdapter") + if !ok { + return nil, &relaymodel.ErrorWithStatusCode{ + StatusCode: http.StatusInternalServerError, + Error: relaymodel.Error{Message: "awsAdapter not found"}, + } } - return a.awsAdapter.DoResponse(c, a.AwsClient, meta) + return adaptor.(utils.AwsAdapter).DoResponse(meta, c) } -func (a *Adaptor) GetModelList() (models []string) { - for model := range adaptors { - models = append(models, model) +func (a *Adaptor) GetModelList() (models []*model.ModelConfig) { + models = make([]*model.ModelConfig, 0, len(adaptors)) + for _, model := range adaptors { + models = append(models, model.config) } return } @@ -76,17 +53,10 @@ func (a *Adaptor) GetRequestURL(_ *meta.Meta) (string, error) { return "", nil } -func (a *Adaptor) SetupRequestHeader(_ *gin.Context, _ *http.Request, _ *meta.Meta) error { +func (a *Adaptor) SetupRequestHeader(_ *meta.Meta, _ *gin.Context, _ *http.Request) error { return nil } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(_ *gin.Context, _ *meta.Meta, _ io.Reader) (*http.Response, error) { +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, _ *http.Request) (*http.Response, error) { return nil, nil } diff --git a/service/aiproxy/relay/adaptor/aws/claude/adapter.go b/service/aiproxy/relay/adaptor/aws/claude/adapter.go index 94e451f9ea5..8b0126f93df 100644 --- a/service/aiproxy/relay/adaptor/aws/claude/adapter.go +++ b/service/aiproxy/relay/adaptor/aws/claude/adapter.go @@ -1,36 +1,38 @@ package aws import ( - "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" + "io" + "net/http" + "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" "github.com/labring/sealos/service/aiproxy/relay/adaptor/anthropic" "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/utils" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" - "github.com/pkg/errors" +) + +const ( + ConvertedRequest = "convertedRequest" ) var _ utils.AwsAdapter = new(Adaptor) type Adaptor struct{} -func (a *Adaptor) ConvertRequest(c *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + r, err := anthropic.ConvertRequest(meta, req) + if err != nil { + return nil, nil, err } - - claudeReq := anthropic.ConvertRequest(request) - c.Set(ctxkey.RequestModel, request.Model) - c.Set(ctxkey.ConvertedRequest, claudeReq) - return claudeReq, nil + meta.Set(ConvertedRequest, r) + return nil, nil, nil } -func (a *Adaptor) DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, awsCli) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context) (usage *model.Usage, err *model.ErrorWithStatusCode) { + if meta.GetBool("stream") { + err, usage = StreamHandler(meta, c) } else { - err, usage = Handler(c, awsCli, meta.ActualModelName) + err, usage = Handler(meta, c) } return } diff --git a/service/aiproxy/relay/adaptor/aws/claude/main.go b/service/aiproxy/relay/adaptor/aws/claude/main.go index bc7fb873f5a..45f974fc908 100644 --- a/service/aiproxy/relay/adaptor/aws/claude/main.go +++ b/service/aiproxy/relay/adaptor/aws/claude/main.go @@ -12,42 +12,88 @@ import ( "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/anthropic" "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/utils" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" "github.com/pkg/errors" ) +type awsModelItem struct { + ID string + model.ModelConfig +} + // AwsModelIDMap maps internal model identifiers to AWS model identifiers. // For more details, see: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html -var AwsModelIDMap = map[string]string{ - "claude-instant-1.2": "anthropic.claude-instant-v1", - "claude-2.0": "anthropic.claude-v2", - "claude-2.1": "anthropic.claude-v2:1", - "claude-3-haiku-20240307": "anthropic.claude-3-haiku-20240307-v1:0", - "claude-3-sonnet-20240229": "anthropic.claude-3-sonnet-20240229-v1:0", - "claude-3-opus-20240229": "anthropic.claude-3-opus-20240229-v1:0", - "claude-3-5-sonnet-20240620": "anthropic.claude-3-5-sonnet-20240620-v1:0", - "claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "claude-3-5-sonnet-latest": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "claude-3-5-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0", + +var AwsModelIDMap = map[string]awsModelItem{ + "claude-instant-1.2": { + ModelConfig: model.ModelConfig{ + Model: "claude-instant-1.2", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + ID: "anthropic.claude-instant-v1", + }, + "claude-2.0": { + ModelConfig: model.ModelConfig{ + Model: "claude-2.0", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + ID: "anthropic.claude-v2", + }, + "claude-2.1": { + ModelConfig: model.ModelConfig{ + Model: "claude-2.1", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + ID: "anthropic.claude-v2:1", + }, + "claude-3-haiku-20240307": { + ModelConfig: model.ModelConfig{ + Model: "claude-3-haiku-20240307", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + ID: "anthropic.claude-3-haiku-20240307-v1:0", + }, + "claude-3-5-sonnet-latest": { + ModelConfig: model.ModelConfig{ + Model: "claude-3-5-sonnet-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + ID: "anthropic.claude-3-5-sonnet-20241022-v2:0", + }, + "claude-3-5-haiku-20241022": { + ModelConfig: model.ModelConfig{ + Model: "claude-3-5-haiku-20241022", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + ID: "anthropic.claude-3-5-haiku-20241022-v1:0", + }, } func awsModelID(requestModel string) (string, error) { if awsModelID, ok := AwsModelIDMap[requestModel]; ok { - return awsModelID, nil + return awsModelID.ID, nil } return "", errors.Errorf("model %s not found", requestModel) } -func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { - awsModelID, err := awsModelID(c.GetString(ctxkey.RequestModel)) +func Handler(meta *meta.Meta, c *gin.Context) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { + awsModelID, err := awsModelID(meta.ActualModelName) if err != nil { return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil } @@ -58,7 +104,7 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* ContentType: aws.String("application/json"), } - convReq, ok := c.Get(ctxkey.ConvertedRequest) + convReq, ok := meta.Get(ConvertedRequest) if !ok { return utils.WrapErr(errors.New("request not found")), nil } @@ -75,7 +121,7 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* return utils.WrapErr(errors.Wrap(err, "marshal request")), nil } - awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) + awsResp, err := meta.AwsClient().InvokeModel(c.Request.Context(), awsReq) if err != nil { return utils.WrapErr(errors.Wrap(err, "InvokeModel")), nil } @@ -87,7 +133,7 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* } openaiResp := anthropic.ResponseClaude2OpenAI(claudeResponse) - openaiResp.Model = modelName + openaiResp.Model = meta.OriginModelName usage := relaymodel.Usage{ PromptTokens: claudeResponse.Usage.InputTokens, CompletionTokens: claudeResponse.Usage.OutputTokens, @@ -99,9 +145,11 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* return nil, &usage } -func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { +func StreamHandler(meta *meta.Meta, c *gin.Context) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { + log := middleware.GetLogger(c) createdTime := helper.GetTimestamp() - awsModelID, err := awsModelID(c.GetString(ctxkey.RequestModel)) + originModelName := meta.OriginModelName + awsModelID, err := awsModelID(meta.ActualModelName) if err != nil { return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil } @@ -112,7 +160,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E ContentType: aws.String("application/json"), } - convReq, ok := c.Get(ctxkey.ConvertedRequest) + convReq, ok := meta.Get(ConvertedRequest) if !ok { return utils.WrapErr(errors.New("request not found")), nil } @@ -129,7 +177,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E return utils.WrapErr(errors.Wrap(err, "marshal request")), nil } - awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq) + awsResp, err := meta.AwsClient().InvokeModelWithResponseStream(c.Request.Context(), awsReq) if err != nil { return utils.WrapErr(errors.Wrap(err, "InvokeModelWithResponseStream")), nil } @@ -139,7 +187,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E c.Writer.Header().Set("Content-Type", "text/event-stream") var usage relaymodel.Usage var id string - var lastToolCallChoice openai.ChatCompletionsStreamResponseChoice + var lastToolCallChoice *openai.ChatCompletionsStreamResponseChoice c.Stream(func(_ io.Writer) bool { event, ok := <-stream.Events() @@ -153,7 +201,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E claudeResp := anthropic.StreamResponse{} err := json.Unmarshal(v.Value.Bytes, &claudeResp) if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) + log.Error("error unmarshalling stream response: " + err.Error()) return false } @@ -168,7 +216,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E id = "chatcmpl-" + meta.ID return true } - if len(lastToolCallChoice.Delta.ToolCalls) > 0 { + if lastToolCallChoice != nil && len(lastToolCallChoice.Delta.ToolCalls) > 0 { lastArgs := &lastToolCallChoice.Delta.ToolCalls[len(lastToolCallChoice.Delta.ToolCalls)-1].Function if len(lastArgs.Arguments) == 0 { // compatible with OpenAI sending an empty object `{}` when no arguments. lastArgs.Arguments = "{}" @@ -178,7 +226,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E } } response.ID = id - response.Model = c.GetString(ctxkey.OriginalModel) + response.Model = originModelName response.Created = createdTime for _, choice := range response.Choices { @@ -188,15 +236,15 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E } err = render.ObjectData(c, response) if err != nil { - logger.SysError("error stream response: " + err.Error()) + log.Error("error stream response: " + err.Error()) return false } return true case *types.UnknownUnionMember: - logger.SysErrorf("unknown tag: %s", v.Tag) + log.Error("unknown tag: " + v.Tag) return false default: - logger.SysErrorf("union is nil or unknown type: %v", v) + log.Errorf("union is nil or unknown type: %v", v) return false } }) diff --git a/service/aiproxy/relay/adaptor/aws/llama3/adapter.go b/service/aiproxy/relay/adaptor/aws/llama3/adapter.go index 3fcef4b8fab..1524a92e7b9 100644 --- a/service/aiproxy/relay/adaptor/aws/llama3/adapter.go +++ b/service/aiproxy/relay/adaptor/aws/llama3/adapter.go @@ -1,36 +1,41 @@ package aws import ( - "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" + "io" + "net/http" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/utils" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" - "github.com/pkg/errors" + relayutils "github.com/labring/sealos/service/aiproxy/relay/utils" +) + +const ( + ConvertedRequest = "convertedRequest" ) var _ utils.AwsAdapter = new(Adaptor) type Adaptor struct{} -func (a *Adaptor) ConvertRequest(c *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := relayutils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err } - + request.Model = meta.ActualModelName + meta.Set("stream", request.Stream) llamaReq := ConvertRequest(request) - c.Set(ctxkey.RequestModel, request.Model) - c.Set(ctxkey.ConvertedRequest, llamaReq) - return llamaReq, nil + meta.Set(ConvertedRequest, llamaReq) + return nil, nil, nil } -func (a *Adaptor) DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, awsCli) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context) (usage *model.Usage, err *model.ErrorWithStatusCode) { + if meta.GetBool("stream") { + err, usage = StreamHandler(meta, c) } else { - err, usage = Handler(c, awsCli, meta.ActualModelName) + err, usage = Handler(meta, c) } return } diff --git a/service/aiproxy/relay/adaptor/aws/llama3/main.go b/service/aiproxy/relay/adaptor/aws/llama3/main.go index 10b56fde851..f60e43a3528 100644 --- a/service/aiproxy/relay/adaptor/aws/llama3/main.go +++ b/service/aiproxy/relay/adaptor/aws/llama3/main.go @@ -3,16 +3,16 @@ package aws import ( "bytes" - "fmt" "io" "net/http" "text/template" json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" "github.com/labring/sealos/service/aiproxy/common/random" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/model" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" @@ -20,24 +20,45 @@ import ( "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/utils" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" ) +type awsModelItem struct { + ID string + model.ModelConfig +} + // AwsModelIDMap maps internal model identifiers to AWS model identifiers. // It currently supports only llama-3-8b and llama-3-70b instruction models. // For more details, see: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html -var AwsModelIDMap = map[string]string{ - "llama3-8b-8192": "meta.llama3-8b-instruct-v1:0", - "llama3-70b-8192": "meta.llama3-70b-instruct-v1:0", +var AwsModelIDMap = map[string]awsModelItem{ + "llama3-8b-8192": { + ModelConfig: model.ModelConfig{ + Model: "llama3-8b-8192", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + ID: "meta.llama3-8b-instruct-v1:0", + }, + "llama3-70b-8192": { + ModelConfig: model.ModelConfig{ + Model: "llama3-70b-8192", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + ID: "meta.llama3-70b-instruct-v1:0", + }, } func awsModelID(requestModel string) (string, error) { if awsModelID, ok := AwsModelIDMap[requestModel]; ok { - return awsModelID, nil + return awsModelID.ID, nil } return "", errors.Errorf("model %s not found", requestModel) @@ -49,11 +70,11 @@ const promptTemplate = `<|begin_of_text|>{{range .Messages}}<|start_header_id|>{ var promptTpl = template.Must(template.New("llama3-chat").Parse(promptTemplate)) -func RenderPrompt(messages []relaymodel.Message) string { +func RenderPrompt(messages []*relaymodel.Message) string { var buf bytes.Buffer - err := promptTpl.Execute(&buf, struct{ Messages []relaymodel.Message }{messages}) + err := promptTpl.Execute(&buf, struct{ Messages []*relaymodel.Message }{messages}) if err != nil { - logger.SysError("error rendering prompt messages: " + err.Error()) + log.Error("error rendering prompt messages: " + err.Error()) } return buf.String() } @@ -72,8 +93,8 @@ func ConvertRequest(textRequest *relaymodel.GeneralOpenAIRequest) *Request { return &llamaRequest } -func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { - awsModelID, err := awsModelID(c.GetString(ctxkey.RequestModel)) +func Handler(meta *meta.Meta, c *gin.Context) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { + awsModelID, err := awsModelID(meta.ActualModelName) if err != nil { return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil } @@ -84,7 +105,7 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* ContentType: aws.String("application/json"), } - llamaReq, ok := c.Get(ctxkey.ConvertedRequest) + llamaReq, ok := meta.Get(ConvertedRequest) if !ok { return utils.WrapErr(errors.New("request not found")), nil } @@ -94,7 +115,7 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* return utils.WrapErr(errors.Wrap(err, "marshal request")), nil } - awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) + awsResp, err := meta.AwsClient().InvokeModel(c.Request.Context(), awsReq) if err != nil { return utils.WrapErr(errors.Wrap(err, "InvokeModel")), nil } @@ -106,7 +127,7 @@ func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (* } openaiResp := ResponseLlama2OpenAI(&llamaResponse) - openaiResp.Model = modelName + openaiResp.Model = meta.OriginModelName usage := relaymodel.Usage{ PromptTokens: llamaResponse.PromptTokenCount, CompletionTokens: llamaResponse.GenerationTokenCount, @@ -136,14 +157,16 @@ func ResponseLlama2OpenAI(llamaResponse *Response) *openai.TextResponse { ID: "chatcmpl-" + random.GetUUID(), Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, + Choices: []*openai.TextResponseChoice{&choice}, } return &fullTextResponse } -func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { +func StreamHandler(meta *meta.Meta, c *gin.Context) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { + log := middleware.GetLogger(c) + createdTime := helper.GetTimestamp() - awsModelID, err := awsModelID(c.GetString(ctxkey.RequestModel)) + awsModelID, err := awsModelID(meta.ActualModelName) if err != nil { return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil } @@ -154,7 +177,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E ContentType: aws.String("application/json"), } - llamaReq, ok := c.Get(ctxkey.ConvertedRequest) + llamaReq, ok := meta.Get(ConvertedRequest) if !ok { return utils.WrapErr(errors.New("request not found")), nil } @@ -164,7 +187,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E return utils.WrapErr(errors.Wrap(err, "marshal request")), nil } - awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq) + awsResp, err := meta.AwsClient().InvokeModelWithResponseStream(c.Request.Context(), awsReq) if err != nil { return utils.WrapErr(errors.Wrap(err, "InvokeModelWithResponseStream")), nil } @@ -185,7 +208,7 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E var llamaResp StreamResponse err := json.Unmarshal(v.Value.Bytes, &llamaResp) if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) + log.Error("error unmarshalling stream response: " + err.Error()) return false } @@ -198,19 +221,19 @@ func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.E } response := StreamResponseLlama2OpenAI(&llamaResp) response.ID = "chatcmpl-" + random.GetUUID() - response.Model = c.GetString(ctxkey.OriginalModel) + response.Model = meta.OriginModelName response.Created = createdTime err = render.ObjectData(c, response) if err != nil { - logger.SysError("error stream response: " + err.Error()) + log.Error("error stream response: " + err.Error()) return true } return true case *types.UnknownUnionMember: - fmt.Println("unknown tag:", v.Tag) + log.Error("unknown tag: " + v.Tag) return false default: - fmt.Println("union is nil or unknown type") + log.Errorf("union is nil or unknown type: %v", v) return false } }) @@ -228,6 +251,6 @@ func StreamResponseLlama2OpenAI(llamaResponse *StreamResponse) *openai.ChatCompl } var openaiResponse openai.ChatCompletionsStreamResponse openaiResponse.Object = "chat.completion.chunk" - openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + openaiResponse.Choices = []*openai.ChatCompletionsStreamResponseChoice{&choice} return &openaiResponse } diff --git a/service/aiproxy/relay/adaptor/aws/llama3/main_test.go b/service/aiproxy/relay/adaptor/aws/llama3/main_test.go index 22b7aa1ca7d..79d222ef6ff 100644 --- a/service/aiproxy/relay/adaptor/aws/llama3/main_test.go +++ b/service/aiproxy/relay/adaptor/aws/llama3/main_test.go @@ -9,7 +9,7 @@ import ( ) func TestRenderPrompt(t *testing.T) { - messages := []relaymodel.Message{ + messages := []*relaymodel.Message{ { Role: "user", Content: "What's your name?", @@ -20,7 +20,7 @@ func TestRenderPrompt(t *testing.T) { ` assert.Equal(t, expected, prompt) - messages = []relaymodel.Message{ + messages = []*relaymodel.Message{ { Role: "system", Content: "Your name is Kat. You are a detective.", diff --git a/service/aiproxy/relay/adaptor/aws/registry.go b/service/aiproxy/relay/adaptor/aws/registry.go index 32083fad9fa..7341666182d 100644 --- a/service/aiproxy/relay/adaptor/aws/registry.go +++ b/service/aiproxy/relay/adaptor/aws/registry.go @@ -1,6 +1,7 @@ package aws import ( + "github.com/labring/sealos/service/aiproxy/model" claude "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/claude" llama3 "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/llama3" "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws/utils" @@ -13,20 +14,25 @@ const ( AwsLlama3 ) -var adaptors = map[string]ModelType{} +type Model struct { + config *model.ModelConfig + _type ModelType +} + +var adaptors = map[string]Model{} func init() { - for model := range claude.AwsModelIDMap { - adaptors[model] = AwsClaude + for _, model := range claude.AwsModelIDMap { + adaptors[model.Model] = Model{config: &model.ModelConfig, _type: AwsClaude} } - for model := range llama3.AwsModelIDMap { - adaptors[model] = AwsLlama3 + for _, model := range llama3.AwsModelIDMap { + adaptors[model.Model] = Model{config: &model.ModelConfig, _type: AwsLlama3} } } func GetAdaptor(model string) utils.AwsAdapter { adaptorType := adaptors[model] - switch adaptorType { + switch adaptorType._type { case AwsClaude: return &claude.Adaptor{} case AwsLlama3: diff --git a/service/aiproxy/relay/adaptor/aws/utils/adaptor.go b/service/aiproxy/relay/adaptor/aws/utils/adaptor.go index 1af4579e967..d034d86f1ba 100644 --- a/service/aiproxy/relay/adaptor/aws/utils/adaptor.go +++ b/service/aiproxy/relay/adaptor/aws/utils/adaptor.go @@ -1,51 +1,15 @@ package utils import ( - "errors" "io" "net/http" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" ) type AwsAdapter interface { - ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) - DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) -} - -type Adaptor struct { - Meta *meta.Meta - AwsClient *bedrockruntime.Client -} - -func (a *Adaptor) Init(meta *meta.Meta) { - a.Meta = meta - a.AwsClient = bedrockruntime.New(bedrockruntime.Options{ - Region: meta.Config.Region, - Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(meta.Config.AK, meta.Config.SK, "")), - }) -} - -func (a *Adaptor) GetRequestURL(_ *meta.Meta) (string, error) { - return "", nil -} - -func (a *Adaptor) SetupRequestHeader(_ *gin.Context, _ *http.Request, _ *meta.Meta) error { - return nil -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(_ *gin.Context, _ *meta.Meta, _ io.Reader) (*http.Response, error) { - return nil, nil + ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) + DoResponse(meta *meta.Meta, c *gin.Context) (usage *model.Usage, err *model.ErrorWithStatusCode) } diff --git a/service/aiproxy/relay/adaptor/azure/constants.go b/service/aiproxy/relay/adaptor/azure/constants.go new file mode 100644 index 00000000000..f0cc9694420 --- /dev/null +++ b/service/aiproxy/relay/adaptor/azure/constants.go @@ -0,0 +1,58 @@ +package azure + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +// func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { +// switch meta.Mode { +// case relaymode.ImagesGenerations: +// // https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api +// // https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2024-03-01-preview +// return fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", meta.Channel.BaseURL, meta.ActualModelName, meta.Channel.Config.APIVersion), nil +// case relaymode.AudioTranscription: +// // https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api +// return fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", meta.Channel.BaseURL, meta.ActualModelName, meta.Channel.Config.APIVersion), nil +// case relaymode.AudioSpeech: +// // https://learn.microsoft.com/en-us/azure/ai-services/openai/text-to-speech-quickstart?tabs=command-line#rest-api +// return fmt.Sprintf("%s/openai/deployments/%s/audio/speech?api-version=%s", meta.Channel.BaseURL, meta.ActualModelName, meta.Channel.Config.APIVersion), nil +// } + +// // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api +// requestURL := strings.Split(meta.RequestURLPath, "?")[0] +// requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, meta.Channel.Config.APIVersion) +// task := strings.TrimPrefix(requestURL, "/v1/") +// model := strings.ReplaceAll(meta.ActualModelName, ".", "") +// // https://github.com/labring/sealos/service/aiproxy/issues/1191 +// // {your endpoint}/openai/deployments/{your azure_model}/chat/completions?api-version={api_version} +// requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model, task) +// return GetFullRequestURL(meta.Channel.BaseURL, requestURL), nil +// } + +func GetFullRequestURL(baseURL string, requestURL string) string { + fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) + + if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") { + fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/v1")) + } + return fullRequestURL +} + +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Api-Key", meta.Channel.Key) + return nil +} + +func (a *Adaptor) GetChannelName() string { + return "azure" +} diff --git a/service/aiproxy/relay/adaptor/baichuan/adaptor.go b/service/aiproxy/relay/adaptor/baichuan/adaptor.go new file mode 100644 index 00000000000..9a7a88adbf2 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baichuan/adaptor.go @@ -0,0 +1,28 @@ +package baichuan + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.baichuan-ai.com" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "baichuan" +} diff --git a/service/aiproxy/relay/adaptor/baichuan/constants.go b/service/aiproxy/relay/adaptor/baichuan/constants.go index cb20a1ffe16..5cab26dcce5 100644 --- a/service/aiproxy/relay/adaptor/baichuan/constants.go +++ b/service/aiproxy/relay/adaptor/baichuan/constants.go @@ -1,7 +1,24 @@ package baichuan -var ModelList = []string{ - "Baichuan2-Turbo", - "Baichuan2-Turbo-192k", - "Baichuan-Text-Embedding", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "Baichuan2-Turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaichuan, + }, + { + Model: "Baichuan2-Turbo-192k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaichuan, + }, + { + Model: "Baichuan-Text-Embedding", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerBaichuan, + }, } diff --git a/service/aiproxy/relay/adaptor/baidu/adaptor.go b/service/aiproxy/relay/adaptor/baidu/adaptor.go index 6befae42296..da6ed65d75b 100644 --- a/service/aiproxy/relay/adaptor/baidu/adaptor.go +++ b/service/aiproxy/relay/adaptor/baidu/adaptor.go @@ -1,139 +1,128 @@ package baidu import ( - "errors" + "context" "fmt" "io" "net/http" "strings" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) type Adaptor struct{} -func (a *Adaptor) Init(_ *meta.Meta) { +const ( + baseURL = "https://aip.baidubce.com" +) + +// Get model-specific endpoint using map +var modelEndpointMap = map[string]string{ + "ERNIE-4.0-8K": "completions_pro", + "ERNIE-4.0": "completions_pro", + "ERNIE-Bot-4": "completions_pro", + "ERNIE-Bot": "completions", + "ERNIE-Bot-turbo": "eb-instant", + "ERNIE-Speed": "ernie_speed", + "ERNIE-3.5-8K": "completions", + "ERNIE-Bot-8K": "ernie_bot_8k", + "ERNIE-Speed-8K": "ernie_speed", + "ERNIE-Lite-8K-0922": "eb-instant", + "ERNIE-Lite-8K-0308": "ernie-lite-8k", + "BLOOMZ-7B": "bloomz_7b1", + "bge-large-zh": "bge_large_zh", + "bge-large-en": "bge_large_en", + "tao-8k": "tao_8k", + "bce-reranker-base_v1": "bce_reranker_base", + "Stable-Diffusion-XL": "sd_xl", + "Fuyu-8B": "fuyu_8b", } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t - suffix := "chat/" - if strings.HasPrefix(meta.ActualModelName, "Embedding") || - strings.HasPrefix(meta.ActualModelName, "bge-large") || - strings.HasPrefix(meta.ActualModelName, "tao-8k") { - suffix = "embeddings/" + // Build base URL + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL } - switch meta.ActualModelName { - case "ERNIE-4.0-8K", "ERNIE-4.0", "ERNIE-Bot-4": - suffix += "completions_pro" - case "ERNIE-Bot": - suffix += "completions" - case "ERNIE-Bot-turbo": - suffix += "eb-instant" - case "ERNIE-Speed": - suffix += "ernie_speed" - case "ERNIE-3.5-8K": - suffix += "completions" - case "ERNIE-3.5-8K-0205": - suffix += "ernie-3.5-8k-0205" - case "ERNIE-3.5-8K-1222": - suffix += "ernie-3.5-8k-1222" - case "ERNIE-Bot-8K": - suffix += "ernie_bot_8k" - case "ERNIE-3.5-4K-0205": - suffix += "ernie-3.5-4k-0205" - case "ERNIE-Speed-8K": - suffix += "ernie_speed" - case "ERNIE-Speed-128K": - suffix += "ernie-speed-128k" - case "ERNIE-Lite-8K-0922": - suffix += "eb-instant" - case "ERNIE-Lite-8K-0308": - suffix += "ernie-lite-8k" - case "ERNIE-Tiny-8K": - suffix += "ernie-tiny-8k" - case "BLOOMZ-7B": - suffix += "bloomz_7b1" - case "Embedding-V1": - suffix += "embedding-v1" - case "bge-large-zh": - suffix += "bge_large_zh" - case "bge-large-en": - suffix += "bge_large_en" - case "tao-8k": - suffix += "tao_8k" - default: - suffix += strings.ToLower(meta.ActualModelName) + + // Get API path suffix based on mode + var pathSuffix string + switch meta.Mode { + case relaymode.ChatCompletions: + pathSuffix = "chat" + case relaymode.Embeddings: + pathSuffix = "embeddings" + case relaymode.Rerank: + pathSuffix = "reranker" + case relaymode.ImagesGenerations: + pathSuffix = "text2image" } - fullRequestURL := fmt.Sprintf("%s/rpc/2.0/ai_custom/v1/wenxinworkshop/%s", meta.BaseURL, suffix) - var accessToken string - var err error - if accessToken, err = GetAccessToken(meta.APIKey); err != nil { - return "", err + + modelEndpoint, ok := modelEndpointMap[meta.ActualModelName] + if !ok { + modelEndpoint = strings.ToLower(meta.ActualModelName) } - fullRequestURL += "?access_token=" + accessToken - return fullRequestURL, nil + + // Construct full URL + fullURL := fmt.Sprintf("%s/rpc/2.0/ai_custom/v1/wenxinworkshop/%s/%s", + meta.Channel.BaseURL, pathSuffix, modelEndpoint) + + return fullURL, nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+meta.Channel.Key) + accessToken, err := GetAccessToken(context.Background(), meta.Channel.Key) + if err != nil { + return err + } + req.URL.RawQuery = "access_token=" + accessToken return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - switch relayMode { +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + switch meta.Mode { case relaymode.Embeddings: - baiduEmbeddingRequest := ConvertEmbeddingRequest(request) - return baiduEmbeddingRequest, nil + meta.Set(openai.MetaEmbeddingsPatchInputToSlices, true) + return openai.ConvertRequest(meta, req) + case relaymode.Rerank: + return openai.ConvertRequest(meta, req) + case relaymode.ImagesGenerations: + return openai.ConvertRequest(meta, req) default: - baiduRequest := ConvertRequest(request) - return baiduRequest, nil + return ConvertRequest(meta, req) } } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp) - } else { - switch meta.Mode { - case relaymode.Embeddings: - err, usage = EmbeddingHandler(c, resp) - default: - err, usage = Handler(c, resp) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.Embeddings: + usage, err = EmbeddingsHandler(meta, c, resp) + case relaymode.Rerank: + usage, err = RerankHandler(meta, c, resp) + case relaymode.ImagesGenerations: + usage, err = ImageHandler(meta, c, resp) + default: + if utils.IsStreamResponse(resp) { + err, usage = StreamHandler(meta, c, resp) + } else { + usage, err = Handler(meta, c, resp) } } return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/baidu/constants.go b/service/aiproxy/relay/adaptor/baidu/constants.go index f952adc6b90..23a188be6d3 100644 --- a/service/aiproxy/relay/adaptor/baidu/constants.go +++ b/service/aiproxy/relay/adaptor/baidu/constants.go @@ -1,20 +1,76 @@ package baidu -var ModelList = []string{ - "ERNIE-4.0-8K", - "ERNIE-3.5-8K", - "ERNIE-3.5-8K-0205", - "ERNIE-3.5-8K-1222", - "ERNIE-Bot-8K", - "ERNIE-3.5-4K-0205", - "ERNIE-Speed-8K", - "ERNIE-Speed-128K", - "ERNIE-Lite-8K-0922", - "ERNIE-Lite-8K-0308", - "ERNIE-Tiny-8K", - "BLOOMZ-7B", - "Embedding-V1", - "bge-large-zh", - "bge-large-en", - "tao-8k", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "BLOOMZ-7B", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.004, + OutputPrice: 0.004, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4800, + }, + }, + + { + Model: "Embedding-V1", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0005, + OutputPrice: 0, + }, + { + Model: "bge-large-zh", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerBAAI, + InputPrice: 0.0005, + OutputPrice: 0, + }, + { + Model: "bge-large-en", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerBAAI, + InputPrice: 0.0005, + OutputPrice: 0, + }, + { + Model: "tao-8k", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0005, + OutputPrice: 0, + }, + + { + Model: "bce-reranker-base_v1", + Type: relaymode.Rerank, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0005, + OutputPrice: 0, + }, + + { + Model: "Stable-Diffusion-XL", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + ImagePrices: map[string]float64{ + "768x768": 0.06, + "576x1024": 0.06, + "1024x576": 0.06, + "768x1024": 0.08, + "1024x768": 0.08, + "1024x1024": 0.08, + "1536x1536": 0.12, + "1152x2048": 0.12, + "2048x1152": 0.12, + "1536x2048": 0.16, + "2048x1536": 0.16, + "2048x2048": 0.16, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/baidu/embeddings.go b/service/aiproxy/relay/adaptor/baidu/embeddings.go new file mode 100644 index 00000000000..9071c871499 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baidu/embeddings.go @@ -0,0 +1,56 @@ +package baidu + +import ( + "io" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +type EmbeddingsResponse struct { + *Error + Usage relaymodel.Usage `json:"usage"` +} + +func EmbeddingsHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + var baiduResponse EmbeddingsResponse + err = json.Unmarshal(body, &baiduResponse) + if err != nil { + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + if baiduResponse.Error != nil && baiduResponse.Error.ErrorCode != 0 { + return &baiduResponse.Usage, openai.ErrorWrapperWithMessage(baiduResponse.Error.ErrorMsg, "baidu_error_"+strconv.Itoa(baiduResponse.Error.ErrorCode), http.StatusInternalServerError) + } + + respMap := make(map[string]any) + err = json.Unmarshal(body, &respMap) + if err != nil { + return &baiduResponse.Usage, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + respMap["model"] = meta.OriginModelName + respMap["object"] = "list" + + data, err := json.Marshal(respMap) + if err != nil { + return &baiduResponse.Usage, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + _, err = c.Writer.Write(data) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return &baiduResponse.Usage, nil +} diff --git a/service/aiproxy/relay/adaptor/baidu/image.go b/service/aiproxy/relay/adaptor/baidu/image.go new file mode 100644 index 00000000000..1d7dc847098 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baidu/image.go @@ -0,0 +1,73 @@ +package baidu + +import ( + "io" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/model" +) + +type ImageData struct { + B64Image string `json:"b64_image"` +} + +type ImageResponse struct { + *Error + ID string `json:"id"` + Data []*ImageData `json:"data"` + Created int64 `json:"created"` +} + +func ImageHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + var imageResponse ImageResponse + err = json.Unmarshal(body, &imageResponse) + if err != nil { + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + + usage := &model.Usage{ + PromptTokens: len(imageResponse.Data), + TotalTokens: len(imageResponse.Data), + } + + if imageResponse.Error != nil && imageResponse.Error.ErrorMsg != "" { + return usage, openai.ErrorWrapperWithMessage(imageResponse.Error.ErrorMsg, "baidu_error_"+strconv.Itoa(imageResponse.Error.ErrorCode), http.StatusBadRequest) + } + + openaiResponse := ToOpenAIImageResponse(&imageResponse) + data, err := json.Marshal(openaiResponse) + if err != nil { + return usage, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + _, err = c.Writer.Write(data) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return usage, nil +} + +func ToOpenAIImageResponse(imageResponse *ImageResponse) *openai.ImageResponse { + response := &openai.ImageResponse{ + Created: imageResponse.Created, + } + for _, data := range imageResponse.Data { + response.Data = append(response.Data, &openai.ImageData{ + B64Json: data.B64Image, + }) + } + return response +} diff --git a/service/aiproxy/relay/adaptor/baidu/main.go b/service/aiproxy/relay/adaptor/baidu/main.go index b9ddf8b869b..f68ecbc5019 100644 --- a/service/aiproxy/relay/adaptor/baidu/main.go +++ b/service/aiproxy/relay/adaptor/baidu/main.go @@ -2,60 +2,51 @@ package baidu import ( "bufio" - "context" - "errors" - "fmt" + "bytes" + "io" "net/http" - "strings" - "sync" - "time" + "strconv" json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/client" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/constant" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2 -type TokenResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` -} - type Message struct { Role string `json:"role"` Content string `json:"content"` } type ChatRequest struct { - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"top_p,omitempty"` - PenaltyScore *float64 `json:"penalty_score,omitempty"` - System string `json:"system,omitempty"` - UserID string `json:"user_id,omitempty"` - Messages []model.Message `json:"messages"` - MaxOutputTokens int `json:"max_output_tokens,omitempty"` - Stream bool `json:"stream,omitempty"` - DisableSearch bool `json:"disable_search,omitempty"` - EnableCitation bool `json:"enable_citation,omitempty"` -} - -type Error struct { - ErrorMsg string `json:"error_msg"` - ErrorCode int `json:"error_code"` + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"top_p,omitempty"` + PenaltyScore *float64 `json:"penalty_score,omitempty"` + System string `json:"system,omitempty"` + UserID string `json:"user_id,omitempty"` + Messages []*model.Message `json:"messages"` + MaxOutputTokens int `json:"max_output_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` + DisableSearch bool `json:"disable_search,omitempty"` + EnableCitation bool `json:"enable_citation,omitempty"` } -var baiduTokenStore sync.Map - -func ConvertRequest(request *model.GeneralOpenAIRequest) *ChatRequest { +func ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err + } + request.Model = meta.ActualModelName baiduRequest := ChatRequest{ Messages: request.Messages, Temperature: request.Temperature, @@ -87,7 +78,12 @@ func ConvertRequest(request *model.GeneralOpenAIRequest) *ChatRequest { break } } - return &baiduRequest + + data, err := json.Marshal(baiduRequest) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(data), nil } func responseBaidu2OpenAI(response *ChatResponse) *openai.TextResponse { @@ -103,13 +99,15 @@ func responseBaidu2OpenAI(response *ChatResponse) *openai.TextResponse { ID: response.ID, Object: "chat.completion", Created: response.Created, - Choices: []openai.TextResponseChoice{choice}, - Usage: response.Usage, + Choices: []*openai.TextResponseChoice{&choice}, + } + if response.Usage != nil { + fullTextResponse.Usage = *response.Usage } return &fullTextResponse } -func streamResponseBaidu2OpenAI(baiduResponse *ChatStreamResponse) *openai.ChatCompletionsStreamResponse { +func streamResponseBaidu2OpenAI(meta *meta.Meta, baiduResponse *ChatStreamResponse) *openai.ChatCompletionsStreamResponse { var choice openai.ChatCompletionsStreamResponseChoice choice.Delta.Content = baiduResponse.Result if baiduResponse.IsEnd { @@ -119,38 +117,18 @@ func streamResponseBaidu2OpenAI(baiduResponse *ChatStreamResponse) *openai.ChatC ID: baiduResponse.ID, Object: "chat.completion.chunk", Created: baiduResponse.Created, - Model: "ernie-bot", - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, + Model: meta.OriginModelName, + Choices: []*openai.ChatCompletionsStreamResponseChoice{&choice}, + Usage: baiduResponse.Usage, } return &response } -func ConvertEmbeddingRequest(request *model.GeneralOpenAIRequest) *EmbeddingRequest { - return &EmbeddingRequest{ - Input: request.ParseInput(), - } -} - -func embeddingResponseBaidu2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { - openAIEmbeddingResponse := openai.EmbeddingResponse{ - Object: "list", - Data: make([]openai.EmbeddingResponseItem, 0, len(response.Data)), - Model: "baidu-embedding", - Usage: response.Usage, - } - for _, item := range response.Data { - openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ - Object: item.Object, - Index: item.Index, - Embedding: item.Embedding, - }) - } - return &openAIEmbeddingResponse -} - -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { +func StreamHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { defer resp.Body.Close() + log := middleware.GetLogger(c) + var usage model.Usage scanner := bufio.NewScanner(resp.Body) scanner.Split(bufio.ScanLines) @@ -171,23 +149,23 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var baiduResponse ChatStreamResponse err := json.Unmarshal(data, &baiduResponse) if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) + log.Error("error unmarshalling stream response: " + err.Error()) continue } - if baiduResponse.Usage.TotalTokens != 0 { + if baiduResponse.Usage != nil { usage.TotalTokens = baiduResponse.Usage.TotalTokens usage.PromptTokens = baiduResponse.Usage.PromptTokens usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens } - response := streamResponseBaidu2OpenAI(&baiduResponse) + response := streamResponseBaidu2OpenAI(meta, &baiduResponse) err = render.ObjectData(c, response) if err != nil { - logger.SysError(err.Error()) + log.Error("error rendering stream response: " + err.Error()) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } render.Done(c) @@ -195,123 +173,25 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC return nil, &usage } -func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { +func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { defer resp.Body.Close() var baiduResponse ChatResponse err := json.NewDecoder(resp.Body).Decode(&baiduResponse) if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } - if baiduResponse.ErrorMsg != "" { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: baiduResponse.ErrorMsg, - Type: "baidu_error", - Param: "", - Code: baiduResponse.ErrorCode, - }, - StatusCode: resp.StatusCode, - }, nil + if baiduResponse.Error != nil && baiduResponse.Error.ErrorCode != 0 { + return nil, openai.ErrorWrapperWithMessage(baiduResponse.Error.ErrorMsg, "baidu_error_"+strconv.Itoa(baiduResponse.Error.ErrorCode), http.StatusInternalServerError) } fullTextResponse := responseBaidu2OpenAI(&baiduResponse) - fullTextResponse.Model = "ernie-bot" - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, &fullTextResponse.Usage -} - -func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var baiduResponse EmbeddingResponse - err := json.NewDecoder(resp.Body).Decode(&baiduResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - if baiduResponse.ErrorMsg != "" { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: baiduResponse.ErrorMsg, - Type: "baidu_error", - Param: "", - Code: baiduResponse.ErrorCode, - }, - StatusCode: resp.StatusCode, - }, nil - } - fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse) + fullTextResponse.Model = meta.OriginModelName jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) } c.Writer.Header().Set("Content-Type", "application/json") c.Writer.WriteHeader(resp.StatusCode) _, _ = c.Writer.Write(jsonResponse) - return nil, &fullTextResponse.Usage -} - -func GetAccessToken(apiKey string) (string, error) { - if val, ok := baiduTokenStore.Load(apiKey); ok { - var accessToken AccessToken - if accessToken, ok = val.(AccessToken); ok { - // soon this will expire - if time.Now().Add(time.Hour).After(accessToken.ExpiresAt) { - go func() { - _, _ = getBaiduAccessTokenHelper(apiKey) - }() - } - return accessToken.AccessToken, nil - } - } - accessToken, err := getBaiduAccessTokenHelper(apiKey) - if err != nil { - return "", err - } - if accessToken == nil { - return "", errors.New("GetAccessToken return a nil token") - } - return accessToken.AccessToken, nil -} - -func getBaiduAccessTokenHelper(apiKey string) (*AccessToken, error) { - parts := strings.Split(apiKey, "|") - if len(parts) != 2 { - return nil, errors.New("invalid baidu apikey") - } - req, err := http.NewRequestWithContext(context.Background(), - http.MethodPost, - fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s", - parts[0], parts[1]), - nil) - if err != nil { - return nil, err - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - res, err := client.ImpatientHTTPClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - var accessToken AccessToken - err = json.NewDecoder(res.Body).Decode(&accessToken) - if err != nil { - return nil, err - } - if accessToken.Error != "" { - return nil, errors.New(accessToken.Error + ": " + accessToken.ErrorDescription) - } - if accessToken.AccessToken == "" { - return nil, errors.New("getBaiduAccessTokenHelper get empty access token") - } - accessToken.ExpiresAt = time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second) - baiduTokenStore.Store(apiKey, accessToken) - return &accessToken, nil + return &fullTextResponse.Usage, nil } diff --git a/service/aiproxy/relay/adaptor/baidu/model.go b/service/aiproxy/relay/adaptor/baidu/model.go index 0f0d52a298c..cff51464192 100644 --- a/service/aiproxy/relay/adaptor/baidu/model.go +++ b/service/aiproxy/relay/adaptor/baidu/model.go @@ -1,20 +1,28 @@ package baidu import ( - "time" - "github.com/labring/sealos/service/aiproxy/relay/model" ) -type ChatResponse struct { +type Error struct { + ErrorMsg string `json:"error_msg"` + ErrorCode int `json:"error_code"` +} + +type ErrorResponse struct { + *Error `json:"error"` ID string `json:"id"` - Object string `json:"object"` - Result string `json:"result"` - Error - Usage model.Usage `json:"usage"` - Created int64 `json:"created"` - IsTruncated bool `json:"is_truncated"` - NeedClearHistory bool `json:"need_clear_history"` +} + +type ChatResponse struct { + Usage *model.Usage `json:"usage"` + *Error `json:"error"` + ID string `json:"id"` + Object string `json:"object"` + Result string `json:"result"` + Created int64 `json:"created"` + IsTruncated bool `json:"is_truncated"` + NeedClearHistory bool `json:"need_clear_history"` } type ChatStreamResponse struct { @@ -22,30 +30,3 @@ type ChatStreamResponse struct { SentenceID int `json:"sentence_id"` IsEnd bool `json:"is_end"` } - -type EmbeddingRequest struct { - Input []string `json:"input"` -} - -type EmbeddingData struct { - Object string `json:"object"` - Embedding []float64 `json:"embedding"` - Index int `json:"index"` -} - -type EmbeddingResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Data []EmbeddingData `json:"data"` - Error - Usage model.Usage `json:"usage"` - Created int64 `json:"created"` -} - -type AccessToken struct { - ExpiresAt time.Time `json:"-"` - AccessToken string `json:"access_token"` - Error string `json:"error,omitempty"` - ErrorDescription string `json:"error_description,omitempty"` - ExpiresIn int64 `json:"expires_in,omitempty"` -} diff --git a/service/aiproxy/relay/adaptor/baidu/rerank.go b/service/aiproxy/relay/adaptor/baidu/rerank.go new file mode 100644 index 00000000000..fe81e9e84e0 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baidu/rerank.go @@ -0,0 +1,62 @@ +package baidu + +import ( + "io" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/model" +) + +type RerankResponse struct { + Error *Error `json:"error"` + Usage model.Usage `json:"usage"` +} + +func RerankHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + reRankResp := &RerankResponse{} + err = json.Unmarshal(respBody, reRankResp) + if err != nil { + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + if reRankResp.Error != nil && reRankResp.Error.ErrorCode != 0 { + return nil, openai.ErrorWrapperWithMessage(reRankResp.Error.ErrorMsg, "baidu_error_"+strconv.Itoa(reRankResp.Error.ErrorCode), http.StatusInternalServerError) + } + respMap := make(map[string]any) + err = json.Unmarshal(respBody, &respMap) + if err != nil { + return &reRankResp.Usage, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + delete(respMap, "model") + delete(respMap, "usage") + respMap["meta"] = &model.RerankMeta{ + Tokens: &model.RerankMetaTokens{ + InputTokens: reRankResp.Usage.TotalTokens, + OutputTokens: 0, + }, + } + respMap["result"] = respMap["results"] + delete(respMap, "results") + jsonData, err := json.Marshal(respMap) + if err != nil { + return &reRankResp.Usage, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + _, err = c.Writer.Write(jsonData) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return &reRankResp.Usage, nil +} diff --git a/service/aiproxy/relay/adaptor/baidu/token.go b/service/aiproxy/relay/adaptor/baidu/token.go new file mode 100644 index 00000000000..d1305dc2c75 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baidu/token.go @@ -0,0 +1,89 @@ +package baidu + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common/client" + log "github.com/sirupsen/logrus" +) + +type AccessToken struct { + ExpiresAt time.Time `json:"-"` + AccessToken string `json:"access_token"` + Error string `json:"error,omitempty"` + ErrorDescription string `json:"error_description,omitempty"` + ExpiresIn int64 `json:"expires_in,omitempty"` +} + +var baiduTokenStore sync.Map + +func GetAccessToken(ctx context.Context, apiKey string) (string, error) { + if val, ok := baiduTokenStore.Load(apiKey); ok { + var accessToken AccessToken + if accessToken, ok = val.(AccessToken); ok { + // soon this will expire + if time.Now().Add(time.Hour).After(accessToken.ExpiresAt) { + go func() { + _, err := getBaiduAccessTokenHelper(context.Background(), apiKey) + if err != nil { + log.Errorf("get baidu access token failed: %v", err) + } + }() + } + return accessToken.AccessToken, nil + } + } + accessToken, err := getBaiduAccessTokenHelper(ctx, apiKey) + if err != nil { + log.Errorf("get baidu access token failed: %v", err) + return "", errors.New("get baidu access token failed") + } + if accessToken == nil { + return "", errors.New("get baidu access token return a nil token") + } + return accessToken.AccessToken, nil +} + +func getBaiduAccessTokenHelper(ctx context.Context, apiKey string) (*AccessToken, error) { + parts := strings.Split(apiKey, "|") + if len(parts) != 2 { + return nil, errors.New("invalid baidu apikey") + } + req, err := http.NewRequestWithContext(ctx, + http.MethodPost, + fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s", + parts[0], parts[1]), + nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + res, err := client.ImpatientHTTPClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var accessToken AccessToken + err = json.NewDecoder(res.Body).Decode(&accessToken) + if err != nil { + return nil, err + } + if accessToken.Error != "" { + return nil, errors.New(accessToken.Error + ": " + accessToken.ErrorDescription) + } + if accessToken.AccessToken == "" { + return nil, errors.New("get baidu access token return empty access token") + } + accessToken.ExpiresAt = time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second) + baiduTokenStore.Store(apiKey, accessToken) + return &accessToken, nil +} diff --git a/service/aiproxy/relay/adaptor/baiduv2/adaptor.go b/service/aiproxy/relay/adaptor/baiduv2/adaptor.go new file mode 100644 index 00000000000..f1c1c487f18 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baiduv2/adaptor.go @@ -0,0 +1,114 @@ +package baiduv2 + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" + + "github.com/gin-gonic/gin" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +type Adaptor struct{} + +const ( + baseURL = "https://qianfan.baidubce.com" +) + +// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu +var v2ModelMap = map[string]string{ + "ERNIE-4.0-8K-Latest": "ernie-4.0-8k-latest", + "ERNIE-4.0-8K-Preview": "ernie-4.0-8k-preview", + "ERNIE-4.0-8K": "ernie-4.0-8k", + "ERNIE-4.0-Turbo-8K-Latest": "ernie-4.0-turbo-8k-latest", + "ERNIE-4.0-Turbo-8K-Preview": "ernie-4.0-turbo-8k-preview", + "ERNIE-4.0-Turbo-8K": "ernie-4.0-turbo-8k", + "ERNIE-4.0-Turbo-128K": "ernie-4.0-turbo-128k", + "ERNIE-3.5-8K-Preview": "ernie-3.5-8k-preview", + "ERNIE-3.5-8K": "ernie-3.5-8k", + "ERNIE-3.5-128K": "ernie-3.5-128k", + "ERNIE-Speed-8K": "ernie-speed-8k", + "ERNIE-Speed-128K": "ernie-speed-128k", + "ERNIE-Speed-Pro-128K": "ernie-speed-pro-128k", + "ERNIE-Lite-8K": "ernie-lite-8k", + "ERNIE-Lite-Pro-128K": "ernie-lite-pro-128k", + "ERNIE-Tiny-8K": "ernie-tiny-8k", + "ERNIE-Character-8K": "ernie-char-8k", + "ERNIE-Character-Fiction-8K": "ernie-char-fiction-8k", + "ERNIE-Novel-8K": "ernie-novel-8k", +} + +func toV2ModelName(modelName string) string { + if v2Model, ok := v2ModelMap[modelName]; ok { + return v2Model + } + return strings.ToLower(modelName) +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + + switch meta.Mode { + case relaymode.ChatCompletions: + return meta.Channel.BaseURL + "/v2/chat/completions", nil + default: + return "", fmt.Errorf("unsupported mode: %d", meta.Mode) + } +} + +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + token, err := GetBearerToken(context.Background(), meta.Channel.Key) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+token.Token) + return nil +} + +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + switch meta.Mode { + case relaymode.ChatCompletions: + actModel := meta.ActualModelName + v2Model := toV2ModelName(actModel) + meta.ActualModelName = v2Model + defer func() { meta.ActualModelName = actModel }() + return openai.ConvertRequest(meta, req) + default: + return nil, nil, fmt.Errorf("unsupported mode: %d", meta.Mode) + } +} + +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) +} + +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.ChatCompletions: + return openai.DoResponse(meta, c, resp) + default: + return nil, openai.ErrorWrapperWithMessage( + fmt.Sprintf("unsupported mode: %d", meta.Mode), + nil, + http.StatusBadRequest, + ) + } +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "baidu v2" +} diff --git a/service/aiproxy/relay/adaptor/baiduv2/constants.go b/service/aiproxy/relay/adaptor/baiduv2/constants.go new file mode 100644 index 00000000000..7d11339a440 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baiduv2/constants.go @@ -0,0 +1,243 @@ +package baiduv2 + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "ERNIE-4.0-8K-Preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.03, + OutputPrice: 0.09, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 5120, + model.ModelConfigMaxInputTokensKey: 5120, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-4.0-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.03, + OutputPrice: 0.09, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 5120, + model.ModelConfigMaxInputTokensKey: 5120, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-4.0-8K-Latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.03, + OutputPrice: 0.09, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 5120, + model.ModelConfigMaxInputTokensKey: 5120, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-4.0-Turbo-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.02, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-4.0-Turbo-8K-Latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.02, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-4.0-Turbo-8K-Preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.02, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-4.0-Turbo-128K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.02, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 126976, + model.ModelConfigMaxInputTokensKey: 126976, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + { + Model: "ERNIE-3.5-8K-Preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0008, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 5120, + model.ModelConfigMaxInputTokensKey: 5120, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-3.5-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0008, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 5120, + model.ModelConfigMaxInputTokensKey: 5120, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-3.5-128K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0008, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 126976, + model.ModelConfigMaxInputTokensKey: 126976, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + { + Model: "ERNIE-Speed-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 7168, + model.ModelConfigMaxInputTokensKey: 7168, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-Speed-128K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 126976, + model.ModelConfigMaxInputTokensKey: 126976, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "ERNIE-Speed-Pro-128K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 126976, + model.ModelConfigMaxInputTokensKey: 126976, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + { + Model: "ERNIE-Lite-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-Lite-Pro-128K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0002, + OutputPrice: 0.0004, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 126976, + model.ModelConfigMaxInputTokensKey: 126976, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + { + Model: "ERNIE-Tiny-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + + { + Model: "ERNIE-Character-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "ERNIE-Character-Fiction-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 5120, + model.ModelConfigMaxInputTokensKey: 5120, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + + { + Model: "ERNIE-Novel-8K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerBaidu, + InputPrice: 0.04, + OutputPrice: 0.12, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 6144, + model.ModelConfigMaxInputTokensKey: 6144, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, +} diff --git a/service/aiproxy/relay/adaptor/baiduv2/token.go b/service/aiproxy/relay/adaptor/baiduv2/token.go new file mode 100644 index 00000000000..2f8b33dac97 --- /dev/null +++ b/service/aiproxy/relay/adaptor/baiduv2/token.go @@ -0,0 +1,108 @@ +package baiduv2 + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + json "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" +) + +type TokenResponse struct { + ExpireTime time.Time `json:"expireTime"` + Token string `json:"token"` +} + +var baiduTokenStore sync.Map + +func GetBearerToken(ctx context.Context, apiKey string) (*TokenResponse, error) { + parts := strings.Split(apiKey, "|") + if len(parts) != 2 { + return nil, errors.New("invalid baidu apikey") + } + if val, ok := baiduTokenStore.Load("bearer|" + apiKey); ok { + var tokenResponse TokenResponse + if tokenResponse, ok = val.(TokenResponse); ok { + if time.Now().Add(time.Hour).After(tokenResponse.ExpireTime) { + go func() { + _, err := getBaiduAccessTokenHelper(context.Background(), apiKey) + if err != nil { + log.Errorf("get baidu access token failed: %v", err) + } + }() + } + return &tokenResponse, nil + } + } + tokenResponse, err := getBaiduAccessTokenHelper(ctx, apiKey) + if err != nil { + return nil, err + } + return tokenResponse, nil +} + +func getBaiduAccessTokenHelper(ctx context.Context, apiKey string) (*TokenResponse, error) { + parts := strings.Split(apiKey, "|") + if len(parts) != 2 { + return nil, errors.New("invalid baidu apikey") + } + authorization := generateAuthorizationString(parts[0], parts[1]) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://iam.bj.baidubce.com/v1/BCE-BEARER/token", nil) + if err != nil { + return nil, err + } + query := req.URL.Query() + query.Add("expireInSeconds", "86400") + req.URL.RawQuery = query.Encode() + req.Header.Set("Authorization", authorization) + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("get token failed, status code: %d", res.StatusCode) + } + var tokenResponse TokenResponse + err = json.NewDecoder(res.Body).Decode(&tokenResponse) + if err != nil { + return nil, err + } + baiduTokenStore.Store("bearer|"+apiKey, tokenResponse) + return &tokenResponse, nil +} + +func generateAuthorizationString(ak, sk string) string { + httpMethod := http.MethodGet + uri := "/v1/BCE-BEARER/token" + queryString := "expireInSeconds=86400" + hostHeader := "iam.bj.baidubce.com" + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\nhost:%s", httpMethod, uri, queryString, hostHeader) + + timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z") + expirationPeriodInSeconds := 1800 + authStringPrefix := fmt.Sprintf("bce-auth-v1/%s/%s/%d", ak, timestamp, expirationPeriodInSeconds) + + signingKey := hmacSHA256(sk, authStringPrefix) + + signature := hmacSHA256(signingKey, canonicalRequest) + + signedHeaders := "host" + authorization := fmt.Sprintf("%s/%s/%s", authStringPrefix, signedHeaders, signature) + + return authorization +} + +func hmacSHA256(key, data string) string { + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/service/aiproxy/relay/adaptor/cloudflare/adaptor.go b/service/aiproxy/relay/adaptor/cloudflare/adaptor.go index 694ce44c5c2..42c66142837 100644 --- a/service/aiproxy/relay/adaptor/cloudflare/adaptor.go +++ b/service/aiproxy/relay/adaptor/cloudflare/adaptor.go @@ -1,48 +1,39 @@ package cloudflare import ( - "errors" "fmt" - "io" - "net/http" "strings" - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) type Adaptor struct { - meta *meta.Meta + openai.Adaptor } -// ConvertImageRequest implements adaptor.Adaptor. -func (*Adaptor) ConvertImageRequest(_ *model.ImageRequest) (any, error) { - return nil, errors.New("not implemented") -} - -// ConvertImageRequest implements adaptor.Adaptor. - -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} +const baseURL = "https://api.cloudflare.com" // WorkerAI cannot be used across accounts with AIGateWay // https://developers.cloudflare.com/ai-gateway/providers/workersai/#openai-compatible-endpoints // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/workers-ai -func (a *Adaptor) isAIGateWay(baseURL string) bool { +func isAIGateWay(baseURL string) bool { return strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") && strings.HasSuffix(baseURL, "/workers-ai") } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - isAIGateWay := a.isAIGateWay(meta.BaseURL) + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + isAIGateWay := isAIGateWay(u) var urlPrefix string if isAIGateWay { - urlPrefix = meta.BaseURL + urlPrefix = u } else { - urlPrefix = fmt.Sprintf("%s/client/v4/accounts/%s/ai", meta.BaseURL, meta.Config.UserID) + urlPrefix = fmt.Sprintf("%s/client/v4/accounts/%s/ai", u, meta.Channel.Config.UserID) } switch meta.Mode { @@ -58,48 +49,7 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { } } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - switch relayMode { - case relaymode.Completions: - return ConvertCompletionsRequest(request), nil - case relaymode.ChatCompletions, relaymode.Embeddings: - return request, nil - default: - return nil, errors.New("not implemented") - } -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp, meta.PromptTokens, meta.ActualModelName) - } else { - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) - } - return -} - -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/cloudflare/constant.go b/service/aiproxy/relay/adaptor/cloudflare/constant.go index 54052aa6ca1..5aba6dfb51c 100644 --- a/service/aiproxy/relay/adaptor/cloudflare/constant.go +++ b/service/aiproxy/relay/adaptor/cloudflare/constant.go @@ -1,37 +1,159 @@ package cloudflare -var ModelList = []string{ - "@cf/meta/llama-3.1-8b-instruct", - "@cf/meta/llama-2-7b-chat-fp16", - "@cf/meta/llama-2-7b-chat-int8", - "@cf/mistral/mistral-7b-instruct-v0.1", - "@hf/thebloke/deepseek-coder-6.7b-base-awq", - "@hf/thebloke/deepseek-coder-6.7b-instruct-awq", - "@cf/deepseek-ai/deepseek-math-7b-base", - "@cf/deepseek-ai/deepseek-math-7b-instruct", - "@cf/thebloke/discolm-german-7b-v1-awq", - "@cf/tiiuae/falcon-7b-instruct", - "@cf/google/gemma-2b-it-lora", - "@hf/google/gemma-7b-it", - "@cf/google/gemma-7b-it-lora", - "@hf/nousresearch/hermes-2-pro-mistral-7b", - "@hf/thebloke/llama-2-13b-chat-awq", - "@cf/meta-llama/llama-2-7b-chat-hf-lora", - "@cf/meta/llama-3-8b-instruct", - "@hf/thebloke/llamaguard-7b-awq", - "@hf/thebloke/mistral-7b-instruct-v0.1-awq", - "@hf/mistralai/mistral-7b-instruct-v0.2", - "@cf/mistral/mistral-7b-instruct-v0.2-lora", - "@hf/thebloke/neural-chat-7b-v3-1-awq", - "@cf/openchat/openchat-3.5-0106", - "@hf/thebloke/openhermes-2.5-mistral-7b-awq", - "@cf/microsoft/phi-2", - "@cf/qwen/qwen1.5-0.5b-chat", - "@cf/qwen/qwen1.5-1.8b-chat", - "@cf/qwen/qwen1.5-14b-chat-awq", - "@cf/qwen/qwen1.5-7b-chat-awq", - "@cf/defog/sqlcoder-7b-2", - "@hf/nexusflow/starling-lm-7b-beta", - "@cf/tinyllama/tinyllama-1.1b-chat-v1.0", - "@hf/thebloke/zephyr-7b-beta-awq", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "@cf/meta/llama-3.1-8b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@cf/meta/llama-2-7b-chat-fp16", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@cf/meta/llama-2-7b-chat-int8", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@cf/mistral/mistral-7b-instruct-v0.1", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "@hf/thebloke/deepseek-coder-6.7b-base-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + }, + { + Model: "@hf/thebloke/deepseek-coder-6.7b-instruct-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + }, + { + Model: "@cf/deepseek-ai/deepseek-math-7b-base", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + }, + { + Model: "@cf/deepseek-ai/deepseek-math-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + }, + { + Model: "@cf/google/gemma-2b-it-lora", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "@hf/google/gemma-7b-it", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "@cf/google/gemma-7b-it-lora", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "@hf/thebloke/llama-2-13b-chat-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@cf/meta-llama/llama-2-7b-chat-hf-lora", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@cf/meta/llama-3-8b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@hf/thebloke/llamaguard-7b-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@hf/thebloke/mistral-7b-instruct-v0.1-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "@hf/mistralai/mistral-7b-instruct-v0.2", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "@cf/mistral/mistral-7b-instruct-v0.2-lora", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "@hf/thebloke/neural-chat-7b-v3-1-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "@cf/openchat/openchat-3.5-0106", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenChat, + }, + { + Model: "@hf/thebloke/openhermes-2.5-mistral-7b-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "@cf/microsoft/phi-2", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMicrosoft, + }, + { + Model: "@cf/qwen/qwen1.5-0.5b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, + { + Model: "@cf/qwen/qwen1.5-1.8b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, + { + Model: "@cf/qwen/qwen1.5-14b-chat-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, + { + Model: "@cf/qwen/qwen1.5-7b-chat-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, + { + Model: "@cf/defog/sqlcoder-7b-2", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDefog, + }, + { + Model: "@hf/nexusflow/starling-lm-7b-beta", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerNexusFlow, + }, + { + Model: "@cf/tinyllama/tinyllama-1.1b-chat-v1.0", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "@hf/thebloke/zephyr-7b-beta-awq", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, } diff --git a/service/aiproxy/relay/adaptor/cloudflare/main.go b/service/aiproxy/relay/adaptor/cloudflare/main.go deleted file mode 100644 index 87679da7489..00000000000 --- a/service/aiproxy/relay/adaptor/cloudflare/main.go +++ /dev/null @@ -1,106 +0,0 @@ -package cloudflare - -import ( - "bufio" - "net/http" - - json "github.com/json-iterator/go" - - "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" - "github.com/labring/sealos/service/aiproxy/common/render" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -func ConvertCompletionsRequest(textRequest *model.GeneralOpenAIRequest) *Request { - p, _ := textRequest.Prompt.(string) - return &Request{ - Prompt: p, - MaxTokens: textRequest.MaxTokens, - Stream: textRequest.Stream, - Temperature: textRequest.Temperature, - } -} - -func StreamHandler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - scanner := bufio.NewScanner(resp.Body) - scanner.Split(bufio.ScanLines) - - common.SetEventStreamHeaders(c) - id := helper.GetResponseID(c) - responseModel := c.GetString(ctxkey.OriginalModel) - var responseText string - - for scanner.Scan() { - data := scanner.Bytes() - if len(data) < 6 || conv.BytesToString(data[:6]) != "data: " { - continue - } - data = data[6:] - - if conv.BytesToString(data) == "[DONE]" { - break - } - - var response openai.ChatCompletionsStreamResponse - err := json.Unmarshal(data, &response) - if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) - continue - } - for _, v := range response.Choices { - v.Delta.Role = "assistant" - responseText += v.Delta.StringContent() - } - response.ID = id - response.Model = modelName - err = render.ObjectData(c, response) - if err != nil { - logger.SysError(err.Error()) - } - } - - if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) - } - - render.Done(c) - - usage := openai.ResponseText2Usage(responseText, responseModel, promptTokens) - return nil, usage -} - -func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var response openai.TextResponse - err := json.NewDecoder(resp.Body).Decode(&response) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - response.Model = modelName - var responseText string - for _, v := range response.Choices { - responseText += v.Message.Content.(string) - } - usage := openai.ResponseText2Usage(responseText, modelName, promptTokens) - response.Usage = *usage - response.ID = helper.GetResponseID(c) - jsonResponse, err := json.Marshal(response) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, usage -} diff --git a/service/aiproxy/relay/adaptor/cloudflare/model.go b/service/aiproxy/relay/adaptor/cloudflare/model.go deleted file mode 100644 index 8d1b480192f..00000000000 --- a/service/aiproxy/relay/adaptor/cloudflare/model.go +++ /dev/null @@ -1,13 +0,0 @@ -package cloudflare - -import "github.com/labring/sealos/service/aiproxy/relay/model" - -type Request struct { - Temperature *float64 `json:"temperature,omitempty"` - Lora string `json:"lora,omitempty"` - Prompt string `json:"prompt,omitempty"` - Messages []model.Message `json:"messages,omitempty"` - MaxTokens int `json:"max_tokens,omitempty"` - Raw bool `json:"raw,omitempty"` - Stream bool `json:"stream,omitempty"` -} diff --git a/service/aiproxy/relay/adaptor/cohere/adaptor.go b/service/aiproxy/relay/adaptor/cohere/adaptor.go index 6815c6bb098..93882964de4 100644 --- a/service/aiproxy/relay/adaptor/cohere/adaptor.go +++ b/service/aiproxy/relay/adaptor/cohere/adaptor.go @@ -1,72 +1,74 @@ package cohere import ( + "bytes" "errors" "io" "net/http" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) type Adaptor struct{} -// ConvertImageRequest implements adaptor.Adaptor. -func (*Adaptor) ConvertImageRequest(_ *model.ImageRequest) (any, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) Init(_ *meta.Meta) { -} +const baseURL = "https://api.cohere.ai" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/v1/chat", nil + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + return u + "/v1/chat", nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+meta.Channel.Key) return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err } - return ConvertRequest(request), nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil + request.Model = meta.ActualModelName + requestBody := ConvertRequest(request) + if requestBody == nil { + return nil, nil, errors.New("request body is nil") + } + data, err := json.Marshal(requestBody) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(data), nil } -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp) - return - } +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { switch meta.Mode { case relaymode.Rerank: - err, usage = openai.RerankHandler(c, resp, meta.PromptTokens, meta) + usage, err = openai.RerankHandler(meta, c, resp) default: - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + if utils.IsStreamResponse(resp) { + err, usage = StreamHandler(c, resp) + } else { + err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + } } return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/cohere/constant.go b/service/aiproxy/relay/adaptor/cohere/constant.go index 9e70652ccb9..d39fa7f873a 100644 --- a/service/aiproxy/relay/adaptor/cohere/constant.go +++ b/service/aiproxy/relay/adaptor/cohere/constant.go @@ -1,14 +1,39 @@ package cohere -var ModelList = []string{ - "command", "command-nightly", - "command-light", "command-light-nightly", - "command-r", "command-r-plus", -} +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) -func init() { - num := len(ModelList) - for i := 0; i < num; i++ { - ModelList = append(ModelList, ModelList[i]+"-internet") - } +var ModelList = []*model.ModelConfig{ + { + Model: "command", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerCohere, + }, + { + Model: "command-nightly", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerCohere, + }, + { + Model: "command-light", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerCohere, + }, + { + Model: "command-light-nightly", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerCohere, + }, + { + Model: "command-r", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerCohere, + }, + { + Model: "command-r-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerCohere, + }, } diff --git a/service/aiproxy/relay/adaptor/cohere/main.go b/service/aiproxy/relay/adaptor/cohere/main.go index d40e0056e0a..61a65c51f42 100644 --- a/service/aiproxy/relay/adaptor/cohere/main.go +++ b/service/aiproxy/relay/adaptor/cohere/main.go @@ -9,11 +9,11 @@ import ( json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/model" ) @@ -107,7 +107,7 @@ func StreamResponseCohere2OpenAI(cohereResponse *StreamResponse) (*openai.ChatCo } var openaiResponse openai.ChatCompletionsStreamResponse openaiResponse.Object = "chat.completion.chunk" - openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + openaiResponse.Choices = []*openai.ChatCompletionsStreamResponseChoice{&choice} return &openaiResponse, response } @@ -126,7 +126,7 @@ func ResponseCohere2OpenAI(cohereResponse *Response) *openai.TextResponse { Model: "model", Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, + Choices: []*openai.TextResponseChoice{&choice}, } return &fullTextResponse } @@ -134,6 +134,8 @@ func ResponseCohere2OpenAI(cohereResponse *Response) *openai.TextResponse { func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { defer resp.Body.Close() + log := middleware.GetLogger(c) + createdTime := helper.GetTimestamp() scanner := bufio.NewScanner(resp.Body) scanner.Split(bufio.ScanLines) @@ -148,7 +150,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var cohereResponse StreamResponse err := json.Unmarshal(conv.StringToBytes(data), &cohereResponse) if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) + log.Error("error unmarshalling stream response: " + err.Error()) continue } @@ -168,12 +170,12 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC err = render.ObjectData(c, response) if err != nil { - logger.SysError(err.Error()) + log.Error("error rendering stream response: " + err.Error()) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } render.Done(c) diff --git a/service/aiproxy/relay/adaptor/common.go b/service/aiproxy/relay/adaptor/common.go deleted file mode 100644 index d4d369bb2c1..00000000000 --- a/service/aiproxy/relay/adaptor/common.go +++ /dev/null @@ -1,47 +0,0 @@ -package adaptor - -import ( - "fmt" - "io" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/client" - "github.com/labring/sealos/service/aiproxy/relay/meta" -) - -func SetupCommonRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) { - req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) - req.Header.Set("Accept", c.Request.Header.Get("Accept")) - if meta.IsStream && c.Request.Header.Get("Accept") == "" { - req.Header.Set("Accept", "text/event-stream") - } -} - -func DoRequestHelper(a Adaptor, c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - fullRequestURL, err := a.GetRequestURL(meta) - if err != nil { - return nil, fmt.Errorf("get request url failed: %w", err) - } - req, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, fullRequestURL, requestBody) - if err != nil { - return nil, fmt.Errorf("new request failed: %w", err) - } - err = a.SetupRequestHeader(c, req, meta) - if err != nil { - return nil, fmt.Errorf("setup request header failed: %w", err) - } - resp, err := DoRequest(c, req) - if err != nil { - return nil, fmt.Errorf("do request failed: %w", err) - } - return resp, nil -} - -func DoRequest(_ *gin.Context, req *http.Request) (*http.Response, error) { - resp, err := client.HTTPClient.Do(req) - if err != nil { - return nil, err - } - return resp, nil -} diff --git a/service/aiproxy/relay/adaptor/coze/adaptor.go b/service/aiproxy/relay/adaptor/coze/adaptor.go index 4bb92b3285d..c322d302638 100644 --- a/service/aiproxy/relay/adaptor/coze/adaptor.go +++ b/service/aiproxy/relay/adaptor/coze/adaptor.go @@ -1,65 +1,69 @@ package coze import ( + "bytes" "errors" "io" "net/http" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) -type Adaptor struct { - meta *meta.Meta -} +type Adaptor struct{} -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} +const baseURL = "https://api.coze.com" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/open_api/v2/chat", nil + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + return u + "/open_api/v2/chat", nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+meta.Channel.Key) return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err + } + request.User = meta.Channel.Config.UserID + request.Model = meta.ActualModelName + requestBody := ConvertRequest(request) + if requestBody == nil { + return nil, nil, errors.New("request body is nil") } - request.User = a.meta.Config.UserID - return ConvertRequest(request), nil + data, err := json.Marshal(requestBody) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(data), nil } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { +func (a *Adaptor) ConvertImageRequest(request *relaymodel.ImageRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } return request, nil } -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { var responseText *string - if meta.IsStream { + if utils.IsStreamResponse(resp) { err, responseText = StreamHandler(c, resp) } else { err, responseText = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) @@ -67,14 +71,14 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met if responseText != nil { usage = openai.ResponseText2Usage(*responseText, meta.ActualModelName, meta.PromptTokens) } else { - usage = &model.Usage{} + usage = &relaymodel.Usage{} } usage.PromptTokens = meta.PromptTokens usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/coze/constants.go b/service/aiproxy/relay/adaptor/coze/constants.go index d20fd875804..5a0ea7ebc37 100644 --- a/service/aiproxy/relay/adaptor/coze/constants.go +++ b/service/aiproxy/relay/adaptor/coze/constants.go @@ -1,3 +1,5 @@ package coze -var ModelList = []string{} +import "github.com/labring/sealos/service/aiproxy/model" + +var ModelList = []*model.ModelConfig{} diff --git a/service/aiproxy/relay/adaptor/coze/main.go b/service/aiproxy/relay/adaptor/coze/main.go index da8e57e222d..be7ce381e7c 100644 --- a/service/aiproxy/relay/adaptor/coze/main.go +++ b/service/aiproxy/relay/adaptor/coze/main.go @@ -7,12 +7,12 @@ import ( json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/coze/constant/messagetype" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/model" @@ -74,7 +74,7 @@ func StreamResponseCoze2OpenAI(cozeResponse *StreamResponse) (*openai.ChatComple } var openaiResponse openai.ChatCompletionsStreamResponse openaiResponse.Object = "chat.completion.chunk" - openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + openaiResponse.Choices = []*openai.ChatCompletionsStreamResponseChoice{&choice} openaiResponse.ID = cozeResponse.ConversationID return &openaiResponse, response } @@ -101,7 +101,7 @@ func ResponseCoze2OpenAI(cozeResponse *Response) *openai.TextResponse { Model: "coze-bot", Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, + Choices: []*openai.TextResponseChoice{&choice}, } return &fullTextResponse } @@ -109,6 +109,8 @@ func ResponseCoze2OpenAI(cozeResponse *Response) *openai.TextResponse { func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *string) { defer resp.Body.Close() + log := middleware.GetLogger(c) + var responseText string createdTime := helper.GetTimestamp() scanner := bufio.NewScanner(resp.Body) @@ -131,7 +133,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var cozeResponse StreamResponse err := json.Unmarshal(data, &cozeResponse) if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) + log.Error("error unmarshalling stream response: " + err.Error()) continue } @@ -148,12 +150,12 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC err = render.ObjectData(c, response) if err != nil { - logger.SysError(err.Error()) + log.Error("error rendering stream response: " + err.Error()) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } render.Done(c) @@ -164,6 +166,8 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC func Handler(c *gin.Context, resp *http.Response, _ int, modelName string) (*model.ErrorWithStatusCode, *string) { defer resp.Body.Close() + log := middleware.GetLogger(c) + var cozeResponse Response err := json.NewDecoder(resp.Body).Decode(&cozeResponse) if err != nil { @@ -186,7 +190,10 @@ func Handler(c *gin.Context, resp *http.Response, _ int, modelName string) (*mod } c.Writer.Header().Set("Content-Type", "application/json") c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) + _, err = c.Writer.Write(jsonResponse) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } var responseText string if len(fullTextResponse.Choices) > 0 { responseText = fullTextResponse.Choices[0].Message.StringContent() diff --git a/service/aiproxy/relay/adaptor/deepl/adaptor.go b/service/aiproxy/relay/adaptor/deepl/adaptor.go deleted file mode 100644 index 3973d8bb3c6..00000000000 --- a/service/aiproxy/relay/adaptor/deepl/adaptor.go +++ /dev/null @@ -1,81 +0,0 @@ -package deepl - -import ( - "errors" - "io" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -type Adaptor struct { - meta *meta.Meta - promptText string -} - -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} - -func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/v2/translate", nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "DeepL-Auth-Key "+meta.APIKey) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - convertedRequest, text := ConvertRequest(request) - a.promptText = text - return convertedRequest, nil -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err = StreamHandler(c, resp, meta.ActualModelName) - } else { - err = Handler(c, resp, meta.ActualModelName) - } - promptTokens := len(a.promptText) - usage = &model.Usage{ - PromptTokens: promptTokens, - TotalTokens: promptTokens, - } - return -} - -func (a *Adaptor) GetModelList() []string { - return ModelList -} - -func (a *Adaptor) GetChannelName() string { - return "deepl" -} diff --git a/service/aiproxy/relay/adaptor/deepl/constants.go b/service/aiproxy/relay/adaptor/deepl/constants.go deleted file mode 100644 index 6a4f25454ab..00000000000 --- a/service/aiproxy/relay/adaptor/deepl/constants.go +++ /dev/null @@ -1,9 +0,0 @@ -package deepl - -// https://developers.deepl.com/docs/api-reference/glossaries - -var ModelList = []string{ - "deepl-zh", - "deepl-en", - "deepl-ja", -} diff --git a/service/aiproxy/relay/adaptor/deepl/helper.go b/service/aiproxy/relay/adaptor/deepl/helper.go deleted file mode 100644 index 6d3a914b922..00000000000 --- a/service/aiproxy/relay/adaptor/deepl/helper.go +++ /dev/null @@ -1,11 +0,0 @@ -package deepl - -import "strings" - -func parseLangFromModelName(modelName string) string { - parts := strings.Split(modelName, "-") - if len(parts) == 1 { - return "ZH" - } - return parts[1] -} diff --git a/service/aiproxy/relay/adaptor/deepl/main.go b/service/aiproxy/relay/adaptor/deepl/main.go deleted file mode 100644 index 2ae86e13f97..00000000000 --- a/service/aiproxy/relay/adaptor/deepl/main.go +++ /dev/null @@ -1,117 +0,0 @@ -package deepl - -import ( - "net/http" - - "github.com/gin-gonic/gin" - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/render" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/constant" - "github.com/labring/sealos/service/aiproxy/relay/constant/finishreason" - "github.com/labring/sealos/service/aiproxy/relay/constant/role" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -// https://developers.deepl.com/docs/getting-started/your-first-api-request - -func ConvertRequest(textRequest *model.GeneralOpenAIRequest) (*Request, string) { - var text string - if len(textRequest.Messages) != 0 { - text = textRequest.Messages[len(textRequest.Messages)-1].StringContent() - } - deeplRequest := Request{ - TargetLang: parseLangFromModelName(textRequest.Model), - Text: []string{text}, - } - return &deeplRequest, text -} - -func StreamResponseDeepL2OpenAI(deeplResponse *Response) *openai.ChatCompletionsStreamResponse { - var choice openai.ChatCompletionsStreamResponseChoice - if len(deeplResponse.Translations) != 0 { - choice.Delta.Content = deeplResponse.Translations[0].Text - } - choice.Delta.Role = role.Assistant - choice.FinishReason = &constant.StopFinishReason - openaiResponse := openai.ChatCompletionsStreamResponse{ - Object: constant.StreamObject, - Created: helper.GetTimestamp(), - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, - } - return &openaiResponse -} - -func ResponseDeepL2OpenAI(deeplResponse *Response) *openai.TextResponse { - var responseText string - if len(deeplResponse.Translations) != 0 { - responseText = deeplResponse.Translations[0].Text - } - choice := openai.TextResponseChoice{ - Index: 0, - Message: model.Message{ - Role: role.Assistant, - Content: responseText, - Name: nil, - }, - FinishReason: finishreason.Stop, - } - fullTextResponse := openai.TextResponse{ - Object: constant.NonStreamObject, - Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, - } - return &fullTextResponse -} - -func StreamHandler(c *gin.Context, resp *http.Response, modelName string) *model.ErrorWithStatusCode { - defer resp.Body.Close() - - var deeplResponse Response - err := json.NewDecoder(resp.Body).Decode(&deeplResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) - } - fullTextResponse := StreamResponseDeepL2OpenAI(&deeplResponse) - fullTextResponse.Model = modelName - fullTextResponse.ID = helper.GetResponseID(c) - common.SetEventStreamHeaders(c) - err = render.ObjectData(c, fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "render_response_body_failed", http.StatusInternalServerError) - } - render.Done(c) - return nil -} - -func Handler(c *gin.Context, resp *http.Response, modelName string) *model.ErrorWithStatusCode { - defer resp.Body.Close() - - var deeplResponse Response - err := json.NewDecoder(resp.Body).Decode(&deeplResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) - } - if deeplResponse.Message != "" { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: deeplResponse.Message, - Code: "deepl_error", - }, - StatusCode: resp.StatusCode, - } - } - fullTextResponse := ResponseDeepL2OpenAI(&deeplResponse) - fullTextResponse.Model = modelName - fullTextResponse.ID = helper.GetResponseID(c) - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil -} diff --git a/service/aiproxy/relay/adaptor/deepl/model.go b/service/aiproxy/relay/adaptor/deepl/model.go deleted file mode 100644 index 4f3a3e01d58..00000000000 --- a/service/aiproxy/relay/adaptor/deepl/model.go +++ /dev/null @@ -1,16 +0,0 @@ -package deepl - -type Request struct { - TargetLang string `json:"target_lang"` - Text []string `json:"text"` -} - -type Translation struct { - DetectedSourceLanguage string `json:"detected_source_language,omitempty"` - Text string `json:"text,omitempty"` -} - -type Response struct { - Message string `json:"message,omitempty"` - Translations []Translation `json:"translations,omitempty"` -} diff --git a/service/aiproxy/relay/adaptor/deepseek/adaptor.go b/service/aiproxy/relay/adaptor/deepseek/adaptor.go new file mode 100644 index 00000000000..4acb32802b0 --- /dev/null +++ b/service/aiproxy/relay/adaptor/deepseek/adaptor.go @@ -0,0 +1,31 @@ +package deepseek + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +var _ adaptor.Adaptor = (*Adaptor)(nil) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.deepseek.com" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "deepseek" +} diff --git a/service/aiproxy/relay/adaptor/deepseek/balance.go b/service/aiproxy/relay/adaptor/deepseek/balance.go new file mode 100644 index 00000000000..34ca472ac98 --- /dev/null +++ b/service/aiproxy/relay/adaptor/deepseek/balance.go @@ -0,0 +1,62 @@ +package deepseek + +import ( + "context" + "errors" + "net/http" + "strconv" + + json "github.com/json-iterator/go" + + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" +) + +var _ adaptor.GetBalance = (*Adaptor)(nil) + +func (a *Adaptor) GetBalance(channel *model.Channel) (float64, error) { + u := channel.BaseURL + if u == "" { + u = baseURL + } + url := u + "/user/balance" + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return 0, err + } + req.Header.Set("Authorization", "Bearer "+channel.Key) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + var usage UsageResponse + if err := json.NewDecoder(resp.Body).Decode(&usage); err != nil { + return 0, err + } + index := -1 + for i, balanceInfo := range usage.BalanceInfos { + if balanceInfo.Currency == "CNY" { + index = i + break + } + } + if index == -1 { + return 0, errors.New("currency CNY not found") + } + balance, err := strconv.ParseFloat(usage.BalanceInfos[index].TotalBalance, 64) + if err != nil { + return 0, err + } + return balance, nil +} + +type UsageResponse struct { + BalanceInfos []struct { + Currency string `json:"currency"` + TotalBalance string `json:"total_balance"` + GrantedBalance string `json:"granted_balance"` + ToppedUpBalance string `json:"topped_up_balance"` + } `json:"balance_infos"` + IsAvailable bool `json:"is_available"` +} diff --git a/service/aiproxy/relay/adaptor/deepseek/constants.go b/service/aiproxy/relay/adaptor/deepseek/constants.go index ad840bc2cc0..6271cb354eb 100644 --- a/service/aiproxy/relay/adaptor/deepseek/constants.go +++ b/service/aiproxy/relay/adaptor/deepseek/constants.go @@ -1,6 +1,25 @@ package deepseek -var ModelList = []string{ - "deepseek-chat", - "deepseek-coder", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "deepseek-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + InputPrice: 0.001, + OutputPrice: 0.002, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 64000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "deepseek-coder", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + }, } diff --git a/service/aiproxy/relay/adaptor/doubao/constants.go b/service/aiproxy/relay/adaptor/doubao/constants.go index dbe819dd511..d90289c852d 100644 --- a/service/aiproxy/relay/adaptor/doubao/constants.go +++ b/service/aiproxy/relay/adaptor/doubao/constants.go @@ -1,13 +1,107 @@ package doubao +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://console.volcengine.com/ark/region:ark+cn-beijing/model -var ModelList = []string{ - "Doubao-pro-128k", - "Doubao-pro-32k", - "Doubao-pro-4k", - "Doubao-lite-128k", - "Doubao-lite-32k", - "Doubao-lite-4k", - "Doubao-embedding", +var ModelList = []*model.ModelConfig{ + { + Model: "Doubao-pro-256k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0050, + OutputPrice: 0.0090, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 256000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "Doubao-pro-128k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0050, + OutputPrice: 0.0090, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "Doubao-pro-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0008, + OutputPrice: 0.0020, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32768, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "Doubao-pro-4k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0008, + OutputPrice: 0.0020, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "Doubao-lite-128k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0008, + OutputPrice: 0.0010, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "Doubao-lite-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32768, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "Doubao-lite-4k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0003, + OutputPrice: 0.0006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + { + Model: "Doubao-embedding", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 4096, + }, + }, + { + Model: "Doubao-embedding-large", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.0007, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 4096, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/doubao/main.go b/service/aiproxy/relay/adaptor/doubao/main.go index 9e3cb858574..9d4b7eb42da 100644 --- a/service/aiproxy/relay/adaptor/doubao/main.go +++ b/service/aiproxy/relay/adaptor/doubao/main.go @@ -4,20 +4,44 @@ import ( "fmt" "strings" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) func GetRequestURL(meta *meta.Meta) (string, error) { + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } switch meta.Mode { case relaymode.ChatCompletions: if strings.HasPrefix(meta.ActualModelName, "bot-") { - return meta.BaseURL + "/api/v3/bots/chat/completions", nil + return u + "/api/v3/bots/chat/completions", nil } - return meta.BaseURL + "/api/v3/chat/completions", nil + return u + "/api/v3/chat/completions", nil case relaymode.Embeddings: - return meta.BaseURL + "/api/v3/embeddings", nil + return u + "/api/v3/embeddings", nil default: return "", fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) } } + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://ark.cn-beijing.volces.com" + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return GetRequestURL(meta) +} + +func (a *Adaptor) GetChannelName() string { + return "doubao" +} diff --git a/service/aiproxy/relay/adaptor/doubaoaudio/constants.go b/service/aiproxy/relay/adaptor/doubaoaudio/constants.go new file mode 100644 index 00000000000..0dcc1566aca --- /dev/null +++ b/service/aiproxy/relay/adaptor/doubaoaudio/constants.go @@ -0,0 +1,105 @@ +package doubaoaudio + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +// https://www.volcengine.com/docs/6561/1257543 + +var ModelList = []*model.ModelConfig{ + { + Model: "Doubao-tts", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerDoubao, + InputPrice: 0.5, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigSupportFormatsKey: []string{ + "pcm", + "mp3", + "wav", + "ogg_opus", + }, + model.ModelConfigSupportVoicesKey: []string{ + "zh_female_cancan_mars_bigtts", + "zh_female_qingxinnvsheng_mars_bigtts", + "zh_female_shuangkuaisisi_moon_bigtts", + "zh_male_wennuanahu_moon_bigtts", + "zh_male_shaonianzixin_moon_bigtts", + "zh_female_zhixingnvsheng_mars_bigtts", + "zh_male_qingshuangnanda_mars_bigtts", + "zh_female_linjianvhai_moon_bigtts", + "zh_male_yuanboxiaoshu_moon_bigtts", + "zh_male_yangguangqingnian_moon_bigtts", + "zh_female_tianmeixiaoyuan_moon_bigtts", + "zh_female_qingchezizi_moon_bigtts", + "zh_male_jieshuoxiaoming_moon_bigtts", + "zh_female_kailangjiejie_moon_bigtts", + "zh_male_linjiananhai_moon_bigtts", + "zh_female_tianmeiyueyue_moon_bigtts", + "zh_female_xinlingjitang_moon_bigtts", + "en_male_smith_mars_bigtts", + "en_female_anna_mars_bigtts", + "en_male_adam_mars_bigtts", + "en_female_sarah_mars_bigtts", + "en_male_dryw_mars_bigtts", + "multi_male_jingqiangkanye_moon_bigtts", + "multi_female_shuangkuaisisi_moon_bigtts", + "multi_male_wanqudashu_moon_bigtts", + "multi_female_gaolengyujie_moon_bigtts", + "zh_male_jingqiangkanye_moon_bigtts", + "zh_female_wanwanxiaohe_moon_bigtts", + "zh_female_wanqudashu_moon_bigtts", + "zh_female_daimengchuanmei_moon_bigtts", + "zh_male_guozhoudege_moon_bigtts", + "zh_male_beijingxiaoye_moon_bigtts", + "zh_male_haoyuxiaoge_moon_bigtts", + "zh_male_guangxiyuanzhou_moon_bigtts", + "zh_female_meituojieer_moon_bigtts", + "zh_male_yuzhouzixuan_moon_bigtts", + "zh_male_naiqimengwa_mars_bigtts", + "zh_female_popo_mars_bigtts", + "zh_female_gaolengyujie_moon_bigtts", + "zh_male_aojiaobazong_moon_bigtts", + "zh_female_meilinvyou_moon_bigtts", + "zh_male_shenyeboke_moon_bigtts", + "zh_female_sajiaonvyou_moon_bigtts", + "zh_female_yuanqinvyou_moon_bigtts", + "ICL_zh_female_bingruoshaonv_tob", + "ICL_zh_female_huoponvhai_tob", + "zh_male_dongfanghaoran_moon_bigtts", + "ICL_zh_female_heainainai_tob", + "ICL_zh_female_linjuayi_tob", + "zh_female_wenrouxiaoya_moon_bigtts", + "zh_male_tiancaitongsheng_mars_bigtts", + "zh_male_sunwukong_mars_bigtts", + "zh_male_xionger_mars_bigtts", + "zh_female_peiqi_mars_bigtts", + "zh_female_wuzetian_mars_bigtts", + "zh_female_gujie_mars_bigtts", + "zh_female_yingtaowanzi_mars_bigtts", + "zh_male_chunhui_mars_bigtts", + "zh_female_shaoergushi_mars_bigtts", + "zh_male_silang_mars_bigtts", + "zh_male_jieshuonansheng_mars_bigtts", + "zh_female_jitangmeimei_mars_bigtts", + "zh_female_tiexinnvsheng_mars_bigtts", + "zh_female_qiaopinvsheng_mars_bigtts", + "zh_female_mengyatou_mars_bigtts", + "zh_male_changtianyi_mars_bigtts", + "zh_male_ruyaqingnian_mars_bigtts", + "zh_male_baqiqingshu_mars_bigtts", + "zh_male_qingcang_mars_bigtts", + "zh_female_gufengshaoyu_mars_bigtts", + "zh_female_wenroushunv_mars_bigtts", + }, + }, + }, + + // { + // Model: "Doubao-stt", + // Type: relaymode.AudioTranscription, + // Owner: model.ModelOwnerDoubao, + // InputPrice: 2.3, + // }, +} diff --git a/service/aiproxy/relay/adaptor/doubaoaudio/main.go b/service/aiproxy/relay/adaptor/doubaoaudio/main.go new file mode 100644 index 00000000000..e3855af9d26 --- /dev/null +++ b/service/aiproxy/relay/adaptor/doubaoaudio/main.go @@ -0,0 +1,99 @@ +package doubaoaudio + +import ( + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +func GetRequestURL(meta *meta.Meta) (string, error) { + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + switch meta.Mode { + case relaymode.AudioSpeech: + return u + "/api/v1/tts/ws_binary", nil + default: + return "", fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) + } +} + +type Adaptor struct{} + +const baseURL = "https://openspeech.bytedance.com" + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return GetRequestURL(meta) +} + +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + switch meta.Mode { + case relaymode.AudioSpeech: + return ConvertTTSRequest(meta, req) + default: + return nil, nil, fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) + } +} + +// key格式: app_id|app_token +func getAppIDAndToken(key string) (string, string, error) { + parts := strings.Split(key, "|") + if len(parts) != 2 { + return "", "", errors.New("invalid key format") + } + return parts[0], parts[1], nil +} + +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + switch meta.Mode { + case relaymode.AudioSpeech: + _, token, err := getAppIDAndToken(meta.Channel.Key) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer;"+token) + return nil + default: + return fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) + } +} + +func (a *Adaptor) DoRequest(meta *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + switch meta.Mode { + case relaymode.AudioSpeech: + return TTSDoRequest(meta, req) + default: + return nil, fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) + } +} + +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.AudioSpeech: + return TTSDoResponse(meta, c, resp) + default: + return nil, openai.ErrorWrapperWithMessage( + fmt.Sprintf("unsupported relay mode %d for doubao", meta.Mode), + nil, + http.StatusBadRequest, + ) + } +} + +func (a *Adaptor) GetChannelName() string { + return "doubao audio" +} diff --git a/service/aiproxy/relay/adaptor/doubaoaudio/tts.go b/service/aiproxy/relay/adaptor/doubaoaudio/tts.go new file mode 100644 index 00000000000..85267704de3 --- /dev/null +++ b/service/aiproxy/relay/adaptor/doubaoaudio/tts.go @@ -0,0 +1,286 @@ +package doubaoaudio + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/labring/sealos/service/aiproxy/common/conv" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" +) + +type DoubaoTTSRequest struct { + Request RequestConfig `json:"request"` + App AppConfig `json:"app"` + User UserConfig `json:"user"` + Audio AudioConfig `json:"audio"` +} + +type AppConfig struct { + AppID string `json:"appid"` + Token string `json:"token"` + Cluster string `json:"cluster"` +} + +type UserConfig struct { + UID string `json:"uid,omitempty"` +} + +type AudioConfig struct { + VoiceType string `json:"voice_type"` + Encoding string `json:"encoding"` + SpeedRatio float64 `json:"speed_ratio,omitempty"` + VolumeRatio float64 `json:"volume_ratio,omitempty"` + PitchRatio float64 `json:"pitch_ratio,omitempty"` +} + +type RequestConfig struct { + ReqID string `json:"reqid"` + Text string `json:"text"` + TextType string `json:"text_type"` + Operation string `json:"operation"` +} + +// version: b0001 (4 bits) +// header size: b0001 (4 bits) +// message type: b0001 (Full client request) (4bits) +// message type specific flags: b0000 (none) (4bits) +// message serialization method: b0001 (JSON) (4 bits) +// message compression: b0001 (gzip) (4bits) +// reserved data: 0x00 (1 byte) +var defaultHeader = []byte{0x11, 0x10, 0x11, 0x00} + +//nolint:gosec +func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalTTSRequest(req) + if err != nil { + return nil, nil, err + } + + reqMap, err := utils.UnmarshalMap(req) + if err != nil { + return nil, nil, err + } + + appID, token, err := getAppIDAndToken(meta.Channel.Key) + if err != nil { + return nil, nil, err + } + + doubaoRequest := DoubaoTTSRequest{ + App: AppConfig{ + AppID: appID, + Token: token, + Cluster: "volcano_tts", + }, + User: UserConfig{ + UID: meta.RequestID, + }, + Audio: AudioConfig{ + SpeedRatio: request.Speed, + }, + Request: RequestConfig{ + ReqID: uuid.New().String(), + Text: request.Input, + TextType: "ssml", // plain + Operation: "submit", + }, + } + + if request.Voice == "" { + request.Voice = "zh_female_cancan_mars_bigtts" + } + doubaoRequest.Audio.VoiceType = request.Voice + + if request.ResponseFormat == "" { + request.ResponseFormat = "pcm" + } + doubaoRequest.Audio.Encoding = request.ResponseFormat + + volumeRatio, ok := reqMap["volume_ratio"].(float64) + if ok { + doubaoRequest.Audio.VolumeRatio = volumeRatio + } + pitchRatio, ok := reqMap["pitch_ratio"].(float64) + if ok { + doubaoRequest.Audio.PitchRatio = pitchRatio + } + + data, err := json.Marshal(doubaoRequest) + if err != nil { + return nil, nil, err + } + + compressedData, err := gzipCompress(data) + if err != nil { + return nil, nil, err + } + + payloadArr := make([]byte, 4) + binary.BigEndian.PutUint32(payloadArr, uint32(len(compressedData))) + clientRequest := make([]byte, len(defaultHeader)) + copy(clientRequest, defaultHeader) + clientRequest = append(clientRequest, payloadArr...) + clientRequest = append(clientRequest, compressedData...) + + return nil, bytes.NewReader(clientRequest), nil +} + +func TTSDoRequest(meta *meta.Meta, req *http.Request) (*http.Response, error) { + wsURL := req.URL + wsURL.Scheme = "wss" + + conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), req.Header) + if err != nil { + return nil, err + } + meta.Set("ws_conn", conn) + + writer, err := conn.NextWriter(websocket.BinaryMessage) + if err != nil { + return nil, err + } + defer writer.Close() + + _, err = io.Copy(writer, req.Body) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(nil), + }, nil +} + +func TTSDoResponse(meta *meta.Meta, c *gin.Context, _ *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + log := middleware.GetLogger(c) + + conn := meta.MustGet("ws_conn").(*websocket.Conn) + defer conn.Close() + + usage := &relaymodel.Usage{ + PromptTokens: meta.PromptTokens, + TotalTokens: meta.PromptTokens, + } + + for { + _, message, err := conn.ReadMessage() + if err != nil { + return usage, openai.ErrorWrapperWithMessage(err.Error(), "doubao_wss_read_msg_failed", http.StatusInternalServerError) + } + + resp, err := parseResponse(message) + if err != nil { + return usage, openai.ErrorWrapperWithMessage(err.Error(), "doubao_tts_parse_response_failed", http.StatusInternalServerError) + } + + _, err = c.Writer.Write(resp.Audio) + if err != nil { + log.Error("write tts response chunk failed: " + err.Error()) + } + + if resp.IsLast { + break + } + } + + return usage, nil +} + +func gzipCompress(input []byte) ([]byte, error) { + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write(input) + if err != nil { + _ = w.Close() + return nil, err + } + err = w.Close() + if err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func gzipDecompress(input []byte) ([]byte, error) { + b := bytes.NewBuffer(input) + r, err := gzip.NewReader(b) + if err != nil { + return nil, err + } + out, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return out, nil +} + +type synResp struct { + Audio []byte + IsLast bool +} + +//nolint:gosec +func parseResponse(res []byte) (resp synResp, err error) { + // protoVersion := res[0] >> 4 + headSize := res[0] & 0x0f + messageType := res[1] >> 4 + messageTypeSpecificFlags := res[1] & 0x0f + // serializationMethod := res[2] >> 4 + messageCompression := res[2] & 0x0f + // reserve := res[3] + // headerExtensions := res[4 : headSize*4] + payload := res[headSize*4:] + + // audio-only server response + switch messageType { + case 0xb: + // no sequence number as ACK + if messageTypeSpecificFlags != 0 { + sequenceNumber := int32(binary.BigEndian.Uint32(payload[0:4])) + // payloadSize := int32(binary.BigEndian.Uint32(payload[4:8])) + payload = payload[8:] + resp.Audio = payload + if sequenceNumber < 0 { + resp.IsLast = true + } + } + return + case 0xf: + // code := int32(binary.BigEndian.Uint32(payload[0:4])) + errMsg := payload[8:] + if messageCompression == 1 { + errMsg, err = gzipDecompress(errMsg) + if err != nil { + return + } + } + err = errors.New(conv.BytesToString(errMsg)) + return + case 0xc: + // msgSize = int32(binary.BigEndian.Uint32(payload[0:4])) + // payload = payload[4:] + // if messageCompression == 1 { + // payload, err = gzipDecompress(payload) + // if err != nil { + // return + // } + // } + return + default: + err = errors.New("wrong message type") + return + } +} diff --git a/service/aiproxy/relay/adaptor/gemini/adaptor.go b/service/aiproxy/relay/adaptor/gemini/adaptor.go index 09b9e1296ff..05686c38a39 100644 --- a/service/aiproxy/relay/adaptor/gemini/adaptor.go +++ b/service/aiproxy/relay/adaptor/gemini/adaptor.go @@ -9,20 +9,20 @@ import ( "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/helper" - channelhelper "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) type Adaptor struct{} -func (a *Adaptor) Init(_ *meta.Meta) { -} +const baseURL = "https://generativelanguage.googleapis.com" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - version := helper.AssignOrDefault(meta.Config.APIVersion, config.GetGeminiVersion()) + version := helper.AssignOrDefault(meta.Channel.Config.APIVersion, config.GetGeminiVersion()) var action string switch meta.Mode { case relaymode.Embeddings: @@ -31,68 +31,53 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { action = "generateContent" } - if meta.IsStream { + if meta.GetBool("stream") { action = "streamGenerateContent?alt=sse" } - return fmt.Sprintf("%s/%s/models/%s:%s", meta.BaseURL, version, meta.ActualModelName, action), nil + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + return fmt.Sprintf("%s/%s/models/%s:%s", u, version, meta.ActualModelName, action), nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - channelhelper.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("X-Goog-Api-Key", meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("X-Goog-Api-Key", meta.Channel.Key) return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - switch relayMode { +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + switch meta.Mode { case relaymode.Embeddings: - geminiEmbeddingRequest := ConvertEmbeddingRequest(request) - return geminiEmbeddingRequest, nil + return ConvertEmbeddingRequest(meta, req) + case relaymode.ChatCompletions: + return ConvertRequest(meta, req) default: - geminiRequest := ConvertRequest(request) - return geminiRequest, nil - } -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") + return nil, nil, errors.New("unsupported mode") } - return request, nil } -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return channelhelper.DoRequestHelper(a, c, meta, requestBody) +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - var responseText string - err, responseText = StreamHandler(c, resp) - usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) - } else { - switch meta.Mode { - case relaymode.Embeddings: - err, usage = EmbeddingHandler(c, resp) - default: - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.Embeddings: + usage, err = EmbeddingHandler(c, resp) + case relaymode.ChatCompletions: + if utils.IsStreamResponse(resp) { + usage, err = StreamHandler(meta, c, resp) + } else { + usage, err = Handler(meta, c, resp) } + default: + return nil, openai.ErrorWrapperWithMessage("unsupported mode", "unsupported_mode", http.StatusBadRequest) } return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/gemini/constants.go b/service/aiproxy/relay/adaptor/gemini/constants.go index b0f84dfc556..2988ca8f719 100644 --- a/service/aiproxy/relay/adaptor/gemini/constants.go +++ b/service/aiproxy/relay/adaptor/gemini/constants.go @@ -1,7 +1,41 @@ package gemini +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://ai.google.dev/models/gemini -var ModelList = []string{ - "gemini-pro", "gemini-1.0-pro", "gemini-1.5-flash", "gemini-1.5-pro", "text-embedding-004", "aqa", +var ModelList = []*model.ModelConfig{ + { + Model: "gemini-pro", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemini-1.0-pro", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemini-1.5-flash", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemini-1.5-pro", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "text-embedding-004", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "aqa", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, } diff --git a/service/aiproxy/relay/adaptor/gemini/embeddings.go b/service/aiproxy/relay/adaptor/gemini/embeddings.go new file mode 100644 index 00000000000..1f8f13909d6 --- /dev/null +++ b/service/aiproxy/relay/adaptor/gemini/embeddings.go @@ -0,0 +1,86 @@ +package gemini + +import ( + "bytes" + "io" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" +) + +func ConvertEmbeddingRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err + } + request.Model = meta.ActualModelName + + inputs := request.ParseInput() + requests := make([]EmbeddingRequest, len(inputs)) + model := "models/" + request.Model + + for i, input := range inputs { + requests[i] = EmbeddingRequest{ + Model: model, + Content: ChatContent{ + Parts: []Part{ + { + Text: input, + }, + }, + }, + } + } + + data, err := json.Marshal(BatchEmbeddingRequest{ + Requests: requests, + }) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(data), nil +} + +func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + var geminiEmbeddingResponse EmbeddingResponse + err := json.NewDecoder(resp.Body).Decode(&geminiEmbeddingResponse) + if err != nil { + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + if geminiEmbeddingResponse.Error != nil { + return nil, openai.ErrorWrapperWithMessage(geminiEmbeddingResponse.Error.Message, geminiEmbeddingResponse.Error.Code, resp.StatusCode) + } + fullTextResponse := embeddingResponseGemini2OpenAI(&geminiEmbeddingResponse) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return nil, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, _ = c.Writer.Write(jsonResponse) + return &fullTextResponse.Usage, nil +} + +func embeddingResponseGemini2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { + openAIEmbeddingResponse := openai.EmbeddingResponse{ + Object: "list", + Data: make([]*openai.EmbeddingResponseItem, 0, len(response.Embeddings)), + Model: "gemini-embedding", + Usage: model.Usage{TotalTokens: 0}, + } + for _, item := range response.Embeddings { + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, &openai.EmbeddingResponseItem{ + Object: `embedding`, + Index: 0, + Embedding: item.Values, + }) + } + return &openAIEmbeddingResponse +} diff --git a/service/aiproxy/relay/adaptor/gemini/main.go b/service/aiproxy/relay/adaptor/gemini/main.go index 87922183c80..9bb3dc6bd35 100644 --- a/service/aiproxy/relay/adaptor/gemini/main.go +++ b/service/aiproxy/relay/adaptor/gemini/main.go @@ -2,21 +2,29 @@ package gemini import ( "bufio" + "bytes" + "context" + "fmt" + "io" "net/http" + "strings" json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/helper" "github.com/labring/sealos/service/aiproxy/common/image" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/common/random" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/constant" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" + log "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" ) @@ -32,148 +40,204 @@ var mimeTypeMap = map[string]string{ "text": "text/plain", } -// Setting safety to the lowest possible values since Gemini is already powerless enough -func ConvertRequest(textRequest *model.GeneralOpenAIRequest) *ChatRequest { +type CountTokensResponse struct { + Error *Error `json:"error,omitempty"` + TotalTokens int `json:"totalTokens"` +} + +func buildSafetySettings() []ChatSafetySettings { safetySetting := config.GetGeminiSafetySetting() - geminiRequest := ChatRequest{ - Contents: make([]ChatContent, 0, len(textRequest.Messages)), - SafetySettings: []ChatSafetySettings{ - { - Category: "HARM_CATEGORY_HARASSMENT", - Threshold: safetySetting, - }, - { - Category: "HARM_CATEGORY_HATE_SPEECH", - Threshold: safetySetting, - }, - { - Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - Threshold: safetySetting, - }, - { - Category: "HARM_CATEGORY_DANGEROUS_CONTENT", - Threshold: safetySetting, - }, - }, - GenerationConfig: ChatGenerationConfig{ - Temperature: textRequest.Temperature, - TopP: textRequest.TopP, - MaxOutputTokens: textRequest.MaxTokens, - }, + return []ChatSafetySettings{ + {Category: "HARM_CATEGORY_HARASSMENT", Threshold: safetySetting}, + {Category: "HARM_CATEGORY_HATE_SPEECH", Threshold: safetySetting}, + {Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", Threshold: safetySetting}, + {Category: "HARM_CATEGORY_DANGEROUS_CONTENT", Threshold: safetySetting}, + } +} + +func buildGenerationConfig(textRequest *model.GeneralOpenAIRequest) *ChatGenerationConfig { + config := ChatGenerationConfig{ + Temperature: textRequest.Temperature, + TopP: textRequest.TopP, + MaxOutputTokens: textRequest.MaxTokens, } + if textRequest.ResponseFormat != nil { if mimeType, ok := mimeTypeMap[textRequest.ResponseFormat.Type]; ok { - geminiRequest.GenerationConfig.ResponseMimeType = mimeType + config.ResponseMimeType = mimeType } if textRequest.ResponseFormat.JSONSchema != nil { - geminiRequest.GenerationConfig.ResponseSchema = textRequest.ResponseFormat.JSONSchema.Schema - geminiRequest.GenerationConfig.ResponseMimeType = mimeTypeMap["json_object"] + config.ResponseSchema = textRequest.ResponseFormat.JSONSchema.Schema + config.ResponseMimeType = mimeTypeMap["json_object"] } } + + return &config +} + +func buildTools(textRequest *model.GeneralOpenAIRequest) []ChatTools { if textRequest.Tools != nil { functions := make([]model.Function, 0, len(textRequest.Tools)) for _, tool := range textRequest.Tools { functions = append(functions, tool.Function) } - geminiRequest.Tools = []ChatTools{ - { - FunctionDeclarations: functions, - }, + return []ChatTools{{FunctionDeclarations: functions}} + } + if textRequest.Functions != nil { + return []ChatTools{{FunctionDeclarations: textRequest.Functions}} + } + return nil +} + +func buildMessageParts(ctx context.Context, part model.MessageContent) ([]Part, error) { + if part.Type == model.ContentTypeText { + return []Part{{Text: part.Text}}, nil + } + + if part.Type == model.ContentTypeImageURL { + mimeType, data, err := image.GetImageFromURL(ctx, part.ImageURL.URL) + if err != nil { + return nil, err } - } else if textRequest.Functions != nil { - geminiRequest.Tools = []ChatTools{ - { - FunctionDeclarations: textRequest.Functions, + return []Part{{ + InlineData: &InlineData{ + MimeType: mimeType, + Data: data, }, - } + }}, nil } + + return nil, nil +} + +func buildContents(textRequest *model.GeneralOpenAIRequest, req *http.Request) ([]ChatContent, error) { + contents := make([]ChatContent, 0, len(textRequest.Messages)) shouldAddDummyModelMessage := false + imageNum := 0 + for _, message := range textRequest.Messages { content := ChatContent{ - Role: message.Role, - Parts: []Part{ - { - Text: message.StringContent(), - }, - }, + Role: message.Role, + Parts: make([]Part, 0), } + + // Convert role names + switch content.Role { + case "assistant": + content.Role = "model" + case "system": + content.Role = "user" + shouldAddDummyModelMessage = true + } + + // Process message content openaiContent := message.ParseContent() - var parts []Part - imageNum := 0 for _, part := range openaiContent { - if part.Type == model.ContentTypeText { - parts = append(parts, Part{ - Text: part.Text, - }) - } else if part.Type == model.ContentTypeImageURL { + if part.Type == model.ContentTypeImageURL { imageNum++ if imageNum > VisionMaxImageNum { continue } - mimeType, data, _ := image.GetImageFromURL(part.ImageURL.URL) - parts = append(parts, Part{ - InlineData: &InlineData{ - MimeType: mimeType, - Data: data, - }, - }) } - } - content.Parts = parts - // there's no assistant role in gemini and API shall vomit if Role is not user or model - if content.Role == "assistant" { - content.Role = "model" - } - // Converting system prompt to prompt from user for the same reason - if content.Role == "system" { - content.Role = "user" - shouldAddDummyModelMessage = true + parts, err := buildMessageParts(req.Context(), part) + if err != nil { + return nil, err + } + content.Parts = append(content.Parts, parts...) } - geminiRequest.Contents = append(geminiRequest.Contents, content) - // If a system message is the last message, we need to add a dummy model message to make gemini happy + contents = append(contents, content) + + // Add dummy model message after system message if shouldAddDummyModelMessage { - geminiRequest.Contents = append(geminiRequest.Contents, ChatContent{ - Role: "model", - Parts: []Part{ - { - Text: "Okay", - }, - }, + contents = append(contents, ChatContent{ + Role: "model", + Parts: []Part{{Text: "Okay"}}, }) shouldAddDummyModelMessage = false } } - return &geminiRequest + return contents, nil } -func ConvertEmbeddingRequest(request *model.GeneralOpenAIRequest) *BatchEmbeddingRequest { - inputs := request.ParseInput() - requests := make([]EmbeddingRequest, len(inputs)) - model := "models/" + request.Model - - for i, input := range inputs { - requests[i] = EmbeddingRequest{ - Model: model, - Content: ChatContent{ - Parts: []Part{ - { - Text: input, - }, - }, - }, - } +// Setting safety to the lowest possible values since Gemini is already powerless enough +func ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + textRequest, err := utils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err + } + + textRequest.Model = meta.ActualModelName + meta.Set("stream", textRequest.Stream) + + contents, err := buildContents(textRequest, req) + if err != nil { + return nil, nil, err + } + + tokenCount, err := CountTokens(req.Context(), meta, contents) + if err != nil { + return nil, nil, err + } + meta.PromptTokens = tokenCount + + // Build actual request + geminiRequest := ChatRequest{ + Contents: contents, + SafetySettings: buildSafetySettings(), + GenerationConfig: buildGenerationConfig(textRequest), + Tools: buildTools(textRequest), + } + + data, err := json.Marshal(geminiRequest) + if err != nil { + return nil, nil, err + } + + return nil, bytes.NewReader(data), nil +} + +func CountTokens(ctx context.Context, meta *meta.Meta, chat []ChatContent) (int, error) { + countReq := ChatRequest{ + Contents: chat, + } + countData, err := json.Marshal(countReq) + if err != nil { + return 0, err + } + version := helper.AssignOrDefault(meta.Channel.Config.APIVersion, config.GetGeminiVersion()) + u := meta.Channel.BaseURL + if u == "" { + u = baseURL } + countURL := fmt.Sprintf("%s/%s/models/%s:countTokens", u, version, meta.ActualModelName) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, countURL, bytes.NewReader(countData)) + if err != nil { + return 0, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Goog-Api-Key", meta.Channel.Key) - return &BatchEmbeddingRequest{ - Requests: requests, + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err } + defer resp.Body.Close() + + var tokenCount CountTokensResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenCount); err != nil { + return 0, err + } + if tokenCount.Error != nil { + return 0, fmt.Errorf("count tokens error: %s, code: %d, status: %s", tokenCount.Error.Message, tokenCount.Error.Code, resp.Status) + } + return tokenCount.TotalTokens, nil } type ChatResponse struct { - Candidates []ChatCandidate `json:"candidates"` + Candidates []*ChatCandidate `json:"candidates"` PromptFeedback ChatPromptFeedback `json:"promptFeedback"` } @@ -203,8 +267,8 @@ type ChatPromptFeedback struct { SafetyRatings []ChatSafetyRating `json:"safetyRatings"` } -func getToolCalls(candidate *ChatCandidate) []model.Tool { - var toolCalls []model.Tool +func getToolCalls(candidate *ChatCandidate) []*model.Tool { + var toolCalls []*model.Tool item := candidate.Content.Parts[0] if item.FunctionCall == nil { @@ -212,7 +276,7 @@ func getToolCalls(candidate *ChatCandidate) []model.Tool { } argsBytes, err := json.Marshal(item.FunctionCall.Arguments) if err != nil { - logger.FatalLog("getToolCalls failed: " + err.Error()) + log.Error("getToolCalls failed: " + err.Error()) return toolCalls } toolCall := model.Tool{ @@ -223,7 +287,7 @@ func getToolCalls(candidate *ChatCandidate) []model.Tool { Name: item.FunctionCall.FunctionName, }, } - toolCalls = append(toolCalls, toolCall) + toolCalls = append(toolCalls, &toolCall) return toolCalls } @@ -232,7 +296,7 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { ID: "chatcmpl-" + random.GetUUID(), Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: make([]openai.TextResponseChoice, 0, len(response.Candidates)), + Choices: make([]*openai.TextResponseChoice, 0, len(response.Candidates)), } for i, candidate := range response.Candidates { choice := openai.TextResponseChoice{ @@ -244,7 +308,7 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { } if len(candidate.Content.Parts) > 0 { if candidate.Content.Parts[0].FunctionCall != nil { - choice.Message.ToolCalls = getToolCalls(&candidate) + choice.Message.ToolCalls = getToolCalls(candidate) } else { choice.Message.Content = candidate.Content.Parts[0].Text } @@ -252,12 +316,12 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { choice.Message.Content = "" choice.FinishReason = candidate.FinishReason } - fullTextResponse.Choices = append(fullTextResponse.Choices, choice) + fullTextResponse.Choices = append(fullTextResponse.Choices, &choice) } return &fullTextResponse } -func streamResponseGeminiChat2OpenAI(geminiResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { +func streamResponseGeminiChat2OpenAI(meta *meta.Meta, geminiResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { var choice openai.ChatCompletionsStreamResponseChoice choice.Delta.Content = geminiResponse.GetResponseText() // choice.FinishReason = &constant.StopFinishReason @@ -265,32 +329,18 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *ChatResponse) *openai.ChatC response.ID = "chatcmpl-" + random.GetUUID() response.Created = helper.GetTimestamp() response.Object = "chat.completion.chunk" - response.Model = "gemini" - response.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + response.Model = meta.OriginModelName + response.Choices = []*openai.ChatCompletionsStreamResponseChoice{&choice} return &response } -func embeddingResponseGemini2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { - openAIEmbeddingResponse := openai.EmbeddingResponse{ - Object: "list", - Data: make([]openai.EmbeddingResponseItem, 0, len(response.Embeddings)), - Model: "gemini-embedding", - Usage: model.Usage{TotalTokens: 0}, - } - for _, item := range response.Embeddings { - openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ - Object: `embedding`, - Index: 0, - Embedding: item.Values, - }) - } - return &openAIEmbeddingResponse -} - -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, string) { +func StreamHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { defer resp.Body.Close() - responseText := "" + log := middleware.GetLogger(c) + + responseText := strings.Builder{} + respContent := []ChatContent{} scanner := bufio.NewScanner(resp.Body) scanner.Split(bufio.ScanLines) @@ -310,96 +360,84 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var geminiResponse ChatResponse err := json.Unmarshal(data, &geminiResponse) if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) + log.Error("error unmarshalling stream response: " + err.Error()) continue } - - response := streamResponseGeminiChat2OpenAI(&geminiResponse) + for _, candidate := range geminiResponse.Candidates { + respContent = append(respContent, candidate.Content) + } + response := streamResponseGeminiChat2OpenAI(meta, &geminiResponse) if response == nil { continue } - responseText += response.Choices[0].Delta.StringContent() + responseText.WriteString(response.Choices[0].Delta.StringContent()) err = render.ObjectData(c, response) if err != nil { - logger.SysError(err.Error()) + log.Error("error rendering stream response: " + err.Error()) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } render.Done(c) - return nil, responseText + usage := model.Usage{ + PromptTokens: meta.PromptTokens, + } + + tokenCount, err := CountTokens(c.Request.Context(), meta, respContent) + if err != nil { + log.Error("count tokens failed: " + err.Error()) + usage.CompletionTokens = openai.CountTokenText(responseText.String(), meta.ActualModelName) + } else { + usage.CompletionTokens = tokenCount + } + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + return &usage, nil } -func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { +func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { defer resp.Body.Close() + log := middleware.GetLogger(c) + var geminiResponse ChatResponse err := json.NewDecoder(resp.Body).Decode(&geminiResponse) if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } if len(geminiResponse.Candidates) == 0 { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: "No candidates returned", - Type: "server_error", - Param: "", - Code: 500, - }, - StatusCode: resp.StatusCode, - }, nil + return nil, openai.ErrorWrapperWithMessage("No candidates returned", "gemini_error", resp.StatusCode) } fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse) - fullTextResponse.Model = modelName - completionTokens := openai.CountTokenText(geminiResponse.GetResponseText(), modelName) - usage := model.Usage{ - PromptTokens: promptTokens, - CompletionTokens: completionTokens, - TotalTokens: promptTokens + completionTokens, + fullTextResponse.Model = meta.OriginModelName + respContent := []ChatContent{} + for _, candidate := range geminiResponse.Candidates { + respContent = append(respContent, candidate.Content) } - fullTextResponse.Usage = usage - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, &usage -} -func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var geminiEmbeddingResponse EmbeddingResponse - err := json.NewDecoder(resp.Body).Decode(&geminiEmbeddingResponse) + usage := model.Usage{ + PromptTokens: meta.PromptTokens, + } + tokenCount, err := CountTokens(c.Request.Context(), meta, respContent) if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - if geminiEmbeddingResponse.Error != nil { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: geminiEmbeddingResponse.Error.Message, - Type: "gemini_error", - Param: "", - Code: geminiEmbeddingResponse.Error.Code, - }, - StatusCode: resp.StatusCode, - }, nil + log.Error("count tokens failed: " + err.Error()) + usage.CompletionTokens = openai.CountTokenText(geminiResponse.GetResponseText(), meta.ActualModelName) + } else { + usage.CompletionTokens = tokenCount } - fullTextResponse := embeddingResponseGemini2OpenAI(&geminiEmbeddingResponse) + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + fullTextResponse.Usage = usage jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + return nil, openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) } c.Writer.Header().Set("Content-Type", "application/json") c.Writer.WriteHeader(resp.StatusCode) _, _ = c.Writer.Write(jsonResponse) - return nil, &fullTextResponse.Usage + return &usage, nil } diff --git a/service/aiproxy/relay/adaptor/gemini/model.go b/service/aiproxy/relay/adaptor/gemini/model.go index b69352d16d5..6b60976fa33 100644 --- a/service/aiproxy/relay/adaptor/gemini/model.go +++ b/service/aiproxy/relay/adaptor/gemini/model.go @@ -1,10 +1,10 @@ package gemini type ChatRequest struct { - Contents []ChatContent `json:"contents"` - SafetySettings []ChatSafetySettings `json:"safety_settings,omitempty"` - Tools []ChatTools `json:"tools,omitempty"` - GenerationConfig ChatGenerationConfig `json:"generation_config,omitempty"` + GenerationConfig *ChatGenerationConfig `json:"generation_config,omitempty"` + Contents []ChatContent `json:"contents"` + SafetySettings []ChatSafetySettings `json:"safety_settings,omitempty"` + Tools []ChatTools `json:"tools,omitempty"` } type EmbeddingRequest struct { diff --git a/service/aiproxy/relay/adaptor/groq/adaptor.go b/service/aiproxy/relay/adaptor/groq/adaptor.go new file mode 100644 index 00000000000..2dfb7380a20 --- /dev/null +++ b/service/aiproxy/relay/adaptor/groq/adaptor.go @@ -0,0 +1,28 @@ +package groq + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.groq.com/openai" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "groq" +} diff --git a/service/aiproxy/relay/adaptor/groq/constants.go b/service/aiproxy/relay/adaptor/groq/constants.go index 0864ebe75e3..b7e62278291 100644 --- a/service/aiproxy/relay/adaptor/groq/constants.go +++ b/service/aiproxy/relay/adaptor/groq/constants.go @@ -1,27 +1,116 @@ package groq +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://console.groq.com/docs/models -var ModelList = []string{ - "gemma-7b-it", - "gemma2-9b-it", - "llama-3.1-70b-versatile", - "llama-3.1-8b-instant", - "llama-3.2-11b-text-preview", - "llama-3.2-11b-vision-preview", - "llama-3.2-1b-preview", - "llama-3.2-3b-preview", - "llama-3.2-11b-vision-preview", - "llama-3.2-90b-text-preview", - "llama-3.2-90b-vision-preview", - "llama-guard-3-8b", - "llama3-70b-8192", - "llama3-8b-8192", - "llama3-groq-70b-8192-tool-use-preview", - "llama3-groq-8b-8192-tool-use-preview", - "llava-v1.5-7b-4096-preview", - "mixtral-8x7b-32768", - "distil-whisper-large-v3-en", - "whisper-large-v3", - "whisper-large-v3-turbo", +var ModelList = []*model.ModelConfig{ + { + Model: "gemma-7b-it", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemma2-9b-it", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "llama-3.1-70b-versatile", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.1-8b-instant", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-11b-text-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-11b-vision-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-1b-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-3b-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-11b-vision-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-90b-text-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-3.2-90b-vision-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama-guard-3-8b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama3-70b-8192", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama3-8b-8192", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama3-groq-70b-8192-tool-use-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama3-groq-8b-8192-tool-use-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llava-v1.5-7b-4096-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "mixtral-8x7b-32768", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "distil-whisper-large-v3-en", + Type: relaymode.AudioTranscription, + Owner: model.ModelOwnerHuggingFace, + }, + { + Model: "whisper-large-v3", + Type: relaymode.AudioTranscription, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "whisper-large-v3-turbo", + Type: relaymode.AudioTranscription, + Owner: model.ModelOwnerOpenAI, + }, } diff --git a/service/aiproxy/relay/adaptor/interface.go b/service/aiproxy/relay/adaptor/interface.go index 2429fa075cb..f9e515ec59d 100644 --- a/service/aiproxy/relay/adaptor/interface.go +++ b/service/aiproxy/relay/adaptor/interface.go @@ -5,20 +5,21 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) type Adaptor interface { - Init(meta *meta.Meta) - GetRequestURL(meta *meta.Meta) (string, error) - SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error - ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) - ConvertImageRequest(request *model.ImageRequest) (any, error) - ConvertSTTRequest(request *http.Request) (io.ReadCloser, error) - ConvertTTSRequest(request *model.TextToSpeechRequest) (any, error) - DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) - DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) - GetModelList() []string GetChannelName() string + GetRequestURL(meta *meta.Meta) (string, error) + SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request) error + ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) + DoRequest(meta *meta.Meta, c *gin.Context, req *http.Request) (*http.Response, error) + DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) + GetModelList() []*model.ModelConfig +} + +type GetBalance interface { + GetBalance(channel *model.Channel) (float64, error) } diff --git a/service/aiproxy/relay/adaptor/lingyiwanwu/adaptor.go b/service/aiproxy/relay/adaptor/lingyiwanwu/adaptor.go new file mode 100644 index 00000000000..d271ba594de --- /dev/null +++ b/service/aiproxy/relay/adaptor/lingyiwanwu/adaptor.go @@ -0,0 +1,28 @@ +package lingyiwanwu + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.lingyiwanwu.com" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "lingyiwanwu" +} diff --git a/service/aiproxy/relay/adaptor/lingyiwanwu/constants.go b/service/aiproxy/relay/adaptor/lingyiwanwu/constants.go index 30000e9dc83..bfa063ddb2c 100644 --- a/service/aiproxy/relay/adaptor/lingyiwanwu/constants.go +++ b/service/aiproxy/relay/adaptor/lingyiwanwu/constants.go @@ -1,9 +1,101 @@ package lingyiwanwu +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://platform.lingyiwanwu.com/docs -var ModelList = []string{ - "yi-34b-chat-0205", - "yi-34b-chat-200k", - "yi-vl-plus", +var ModelList = []*model.ModelConfig{ + { + Model: "yi-lightning", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.00099, + OutputPrice: 0.00099, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, + { + Model: "yi-large", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.02, + OutputPrice: 0.02, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32768, + }, + }, + { + Model: "yi-medium", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.0025, + OutputPrice: 0.0025, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, + { + Model: "yi-vision", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.006, + OutputPrice: 0.006, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, + { + Model: "yi-medium-200k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.012, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 204800, + }, + }, + { + Model: "yi-spark", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.001, + OutputPrice: 0.001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, + { + Model: "yi-large-rag", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.025, + OutputPrice: 0.025, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, + { + Model: "yi-large-fc", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.02, + OutputPrice: 0.02, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32768, + }, + }, + { + Model: "yi-large-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerLingyiWanwu, + InputPrice: 0.012, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/minimax/adaptor.go b/service/aiproxy/relay/adaptor/minimax/adaptor.go new file mode 100644 index 00000000000..954e5139dc9 --- /dev/null +++ b/service/aiproxy/relay/adaptor/minimax/adaptor.go @@ -0,0 +1,87 @@ +package minimax + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.minimax.chat" + +func GetAPIKey(key string) string { + keys := strings.Split(key, "|") + if len(keys) > 0 { + return keys[0] + } + return "" +} + +func GetGroupID(key string) string { + keys := strings.Split(key, "|") + if len(keys) > 1 { + return keys[1] + } + return "" +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+GetAPIKey(meta.Channel.Key)) + return nil +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + switch meta.Mode { + case relaymode.ChatCompletions: + return meta.Channel.BaseURL + "/v1/text/chatcompletion_v2", nil + case relaymode.Embeddings: + return fmt.Sprintf("%s/v1/embeddings?GroupId=%s", meta.Channel.BaseURL, GetGroupID(meta.Channel.Key)), nil + case relaymode.AudioSpeech: + return fmt.Sprintf("%s/v1/t2a_v2?GroupId=%s", meta.Channel.BaseURL, GetGroupID(meta.Channel.Key)), nil + default: + return a.Adaptor.GetRequestURL(meta) + } +} + +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + switch meta.Mode { + case relaymode.ChatCompletions: + meta.Set(openai.DoNotPatchStreamOptionsIncludeUsageMetaKey, true) + return a.Adaptor.ConvertRequest(meta, req) + case relaymode.AudioSpeech: + return ConvertTTSRequest(meta, req) + default: + return a.Adaptor.ConvertRequest(meta, req) + } +} + +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.AudioSpeech: + return TTSHandler(meta, c, resp) + default: + return a.Adaptor.DoResponse(meta, c, resp) + } +} + +func (a *Adaptor) GetChannelName() string { + return "minimax" +} diff --git a/service/aiproxy/relay/adaptor/minimax/constants.go b/service/aiproxy/relay/adaptor/minimax/constants.go index 1b2fc10485d..468b0ac0e84 100644 --- a/service/aiproxy/relay/adaptor/minimax/constants.go +++ b/service/aiproxy/relay/adaptor/minimax/constants.go @@ -1,11 +1,96 @@ package minimax +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://www.minimaxi.com/document/guides/chat-model/V2?id=65e0736ab2845de20908e2dd -var ModelList = []string{ - "abab6.5-chat", - "abab6.5s-chat", - "abab6-chat", - "abab5.5-chat", - "abab5.5s-chat", +var ModelList = []*model.ModelConfig{ + { + Model: "abab7-chat-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.01, + OutputPrice: 0.01, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 245760, + }, + }, + { + Model: "abab6.5s-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.001, + OutputPrice: 0.001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 245760, + }, + }, + { + Model: "abab6.5g-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.005, + OutputPrice: 0.005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8192, + }, + }, + { + Model: "abab6.5t-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.005, + OutputPrice: 0.005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8192, + }, + }, + { + Model: "abab5.5s-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.005, + OutputPrice: 0.005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8192, + }, + }, + { + Model: "abab5.5-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.015, + OutputPrice: 0.015, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 16384, + }, + }, + + { + Model: "speech-01-turbo", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerMiniMax, + InputPrice: 0.2, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigSupportFormatsKey: []string{"pcm", "wav", "flac", "mp3"}, + model.ModelConfigSupportVoicesKey: []string{ + "male-qn-qingse", "male-qn-jingying", "male-qn-badao", "male-qn-daxuesheng", + "female-shaonv", "female-yujie", "female-chengshu", "female-tianmei", + "presenter_male", "presenter_female", + "audiobook_male_1", "audiobook_male_2", "audiobook_female_1", "audiobook_female_2", + "male-qn-qingse-jingpin", "male-qn-jingying-jingpin", "male-qn-badao-jingpin", "male-qn-daxuesheng-jingpin", + "female-shaonv-jingpin", "female-yujie-jingpin", "female-chengshu-jingpin", "female-tianmei-jingpin", + "clever_boy", "cute_boy", "lovely_girl", "cartoon_pig", + "bingjiao_didi", "junlang_nanyou", "chunzhen_xuedi", "lengdan_xiongzhang", + "badao_shaoye", "tianxin_xiaoling", "qiaopi_mengmei", "wumei_yujie", + "diadia_xuemei", "danya_xuejie", + "Santa_Claus", "Grinch", "Rudolph", "Arnold", + "Charming_Santa", "Charming_Lady", "Sweet_Girl", "Cute_Elf", + "Attractive_Girl", "Serene_Woman", + }, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/minimax/main.go b/service/aiproxy/relay/adaptor/minimax/main.go deleted file mode 100644 index 13e9bc27c24..00000000000 --- a/service/aiproxy/relay/adaptor/minimax/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package minimax - -import ( - "fmt" - - "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" -) - -func GetRequestURL(meta *meta.Meta) (string, error) { - if meta.Mode == relaymode.ChatCompletions { - return meta.BaseURL + "/v1/text/chatcompletion_v2", nil - } - return "", fmt.Errorf("unsupported relay mode %d for minimax", meta.Mode) -} diff --git a/service/aiproxy/relay/adaptor/minimax/tts.go b/service/aiproxy/relay/adaptor/minimax/tts.go new file mode 100644 index 00000000000..87ec58fa3f7 --- /dev/null +++ b/service/aiproxy/relay/adaptor/minimax/tts.go @@ -0,0 +1,201 @@ +package minimax + +import ( + "bufio" + "bytes" + "encoding/hex" + "io" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common/conv" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" +) + +func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + reqMap, err := utils.UnmarshalMap(req) + if err != nil { + return nil, nil, err + } + + reqMap["model"] = meta.ActualModelName + + reqMap["text"] = reqMap["input"] + delete(reqMap, "input") + + voice, _ := reqMap["voice"].(string) + delete(reqMap, "voice") + if voice == "" { + voice = "male-qn-qingse" + } + + voiceSetting, ok := reqMap["voice_setting"].(map[string]any) + if !ok { + voiceSetting = map[string]any{} + reqMap["voice_setting"] = voiceSetting + } + if timberWeights, ok := reqMap["timber_weights"].([]any); !ok || len(timberWeights) == 0 { + voiceSetting["voice_id"] = voice + } + + speed, ok := reqMap["speed"].(float64) + if ok { + voiceSetting["speed"] = int(speed) + } + delete(reqMap, "speed") + + audioSetting, ok := reqMap["audio_setting"].(map[string]any) + if !ok { + audioSetting = map[string]any{} + reqMap["audio_setting"] = audioSetting + } + + responseFormat, ok := reqMap["response_format"].(string) + if ok && responseFormat != "" { + audioSetting["format"] = responseFormat + } + delete(reqMap, "response_format") + + sampleRate, ok := reqMap["sample_rate"].(float64) + if ok { + audioSetting["sample_rate"] = int(sampleRate) + } + delete(reqMap, "sample_rate") + + if responseFormat == "wav" { + reqMap["stream"] = false + meta.Set("stream", false) + } else { + stream, _ := reqMap["stream"].(bool) + meta.Set("stream", stream) + } + + body, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + + return nil, bytes.NewReader(body), nil +} + +type TTSExtraInfo struct { + AudioFormat string `json:"audio_format"` + UsageCharacters int `json:"usage_characters"` +} + +type TTSBaseResp struct { + StatusMsg string `json:"status_msg"` + StatusCode int `json:"status_code"` +} + +type TTSData struct { + Audio string `json:"audio"` + Status int `json:"status"` +} + +type TTSResponse struct { + BaseResp *TTSBaseResp `json:"base_resp"` + ExtraInfo TTSExtraInfo `json:"extra_info"` + Data TTSData `json:"data"` +} + +func TTSHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") && meta.GetBool("stream") { + return ttsStreamHandler(meta, c, resp) + } + + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, openai.ErrorWrapper(err, "TTS_ERROR", http.StatusInternalServerError) + } + + var result TTSResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, openai.ErrorWrapper(err, "TTS_ERROR", http.StatusInternalServerError) + } + if result.BaseResp != nil && result.BaseResp.StatusCode != 0 { + return nil, openai.ErrorWrapperWithMessage(result.BaseResp.StatusMsg, "TTS_ERROR_"+strconv.Itoa(result.BaseResp.StatusCode), http.StatusInternalServerError) + } + + resp.Header.Set("Content-Type", "audio/"+result.ExtraInfo.AudioFormat) + + audioBytes, err := hex.DecodeString(result.Data.Audio) + if err != nil { + return nil, openai.ErrorWrapper(err, "TTS_ERROR", http.StatusInternalServerError) + } + + _, err = c.Writer.Write(audioBytes) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + + usageCharacters := meta.PromptTokens + if result.ExtraInfo.UsageCharacters > 0 { + usageCharacters = result.ExtraInfo.UsageCharacters + } + + return &relaymodel.Usage{ + PromptTokens: usageCharacters, + TotalTokens: usageCharacters, + }, nil +} + +func ttsStreamHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + defer resp.Body.Close() + + resp.Header.Set("Content-Type", "application/octet-stream") + + log := middleware.GetLogger(c) + + scanner := bufio.NewScanner(resp.Body) + scanner.Split(bufio.ScanLines) + + usageCharacters := meta.PromptTokens + + for scanner.Scan() { + data := scanner.Text() + if len(data) < openai.DataPrefixLength { // ignore blank line or wrong format + continue + } + if data[:openai.DataPrefixLength] != openai.DataPrefix { + continue + } + data = data[openai.DataPrefixLength:] + + var result TTSResponse + if err := json.Unmarshal(conv.StringToBytes(data), &result); err != nil { + log.Error("unmarshal tts response failed: " + err.Error()) + continue + } + if result.ExtraInfo.UsageCharacters > 0 { + usageCharacters = result.ExtraInfo.UsageCharacters + } + + audioBytes, err := hex.DecodeString(result.Data.Audio) + if err != nil { + log.Error("decode audio failed: " + err.Error()) + continue + } + + _, err = c.Writer.Write(audioBytes) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + } + + return &relaymodel.Usage{ + PromptTokens: usageCharacters, + TotalTokens: usageCharacters, + }, nil +} diff --git a/service/aiproxy/relay/adaptor/mistral/adaptor.go b/service/aiproxy/relay/adaptor/mistral/adaptor.go new file mode 100644 index 00000000000..f27bcff71f9 --- /dev/null +++ b/service/aiproxy/relay/adaptor/mistral/adaptor.go @@ -0,0 +1,28 @@ +package mistral + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.mistral.ai" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "mistral" +} diff --git a/service/aiproxy/relay/adaptor/mistral/constants.go b/service/aiproxy/relay/adaptor/mistral/constants.go index cdb157f5721..82ad8a6071d 100644 --- a/service/aiproxy/relay/adaptor/mistral/constants.go +++ b/service/aiproxy/relay/adaptor/mistral/constants.go @@ -1,10 +1,39 @@ package mistral -var ModelList = []string{ - "open-mistral-7b", - "open-mixtral-8x7b", - "mistral-small-latest", - "mistral-medium-latest", - "mistral-large-latest", - "mistral-embed", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "open-mistral-7b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "open-mixtral-8x7b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "mistral-small-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "mistral-medium-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "mistral-large-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "mistral-embed", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerMistral, + }, } diff --git a/service/aiproxy/relay/adaptor/moonshot/adaptor.go b/service/aiproxy/relay/adaptor/moonshot/adaptor.go new file mode 100644 index 00000000000..0ad45bf456d --- /dev/null +++ b/service/aiproxy/relay/adaptor/moonshot/adaptor.go @@ -0,0 +1,28 @@ +package moonshot + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.moonshot.cn" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "moonshot" +} diff --git a/service/aiproxy/relay/adaptor/moonshot/balance.go b/service/aiproxy/relay/adaptor/moonshot/balance.go new file mode 100644 index 00000000000..8e901a52976 --- /dev/null +++ b/service/aiproxy/relay/adaptor/moonshot/balance.go @@ -0,0 +1,56 @@ +package moonshot + +import ( + "context" + "fmt" + "net/http" + + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" +) + +var _ adaptor.GetBalance = (*Adaptor)(nil) + +func (a *Adaptor) GetBalance(channel *model.Channel) (float64, error) { + u := channel.BaseURL + if u == "" { + u = baseURL + } + url := u + "/v1/users/me/balance" + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return 0, err + } + req.Header.Set("Authorization", "Bearer "+channel.Key) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var response BalanceResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return 0, err + } + + if response.Error != nil { + return 0, fmt.Errorf("type: %s, message: %s", response.Error.Type, response.Error.Message) + } + + return response.Data.AvailableBalance, nil +} + +type BalanceResponse struct { + Error *BalanceError `json:"error"` + Data BalanceData `json:"data"` +} + +type BalanceData struct { + AvailableBalance float64 `json:"available_balance"` +} + +type BalanceError struct { + Message string `json:"message"` + Type string `json:"type"` +} diff --git a/service/aiproxy/relay/adaptor/moonshot/constants.go b/service/aiproxy/relay/adaptor/moonshot/constants.go index 1b86f0fa6e4..2a77f4cef50 100644 --- a/service/aiproxy/relay/adaptor/moonshot/constants.go +++ b/service/aiproxy/relay/adaptor/moonshot/constants.go @@ -1,7 +1,39 @@ package moonshot -var ModelList = []string{ - "moonshot-v1-8k", - "moonshot-v1-32k", - "moonshot-v1-128k", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "moonshot-v1-8k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMoonshot, + InputPrice: 0.012, + OutputPrice: 0.012, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 8192, + }, + }, + { + Model: "moonshot-v1-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMoonshot, + InputPrice: 0.024, + OutputPrice: 0.024, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 32768, + }, + }, + { + Model: "moonshot-v1-128k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMoonshot, + InputPrice: 0.06, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 131072, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/novita/adaptor.go b/service/aiproxy/relay/adaptor/novita/adaptor.go new file mode 100644 index 00000000000..1ab848ad4e4 --- /dev/null +++ b/service/aiproxy/relay/adaptor/novita/adaptor.go @@ -0,0 +1,39 @@ +package novita + +import ( + "fmt" + + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +func GetRequestURL(meta *meta.Meta) (string, error) { + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + if meta.Mode == relaymode.ChatCompletions { + return u + "/chat/completions", nil + } + return "", fmt.Errorf("unsupported relay mode %d for novita", meta.Mode) +} + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.novita.ai/v3/openai" + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return GetRequestURL(meta) +} + +func (a *Adaptor) GetChannelName() string { + return "novita" +} diff --git a/service/aiproxy/relay/adaptor/novita/constants.go b/service/aiproxy/relay/adaptor/novita/constants.go index c6618308e22..d5566bf2131 100644 --- a/service/aiproxy/relay/adaptor/novita/constants.go +++ b/service/aiproxy/relay/adaptor/novita/constants.go @@ -1,19 +1,46 @@ package novita +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://novita.ai/llm-api -var ModelList = []string{ - "meta-llama/llama-3-8b-instruct", - "meta-llama/llama-3-70b-instruct", - "nousresearch/hermes-2-pro-llama-3-8b", - "nousresearch/nous-hermes-llama2-13b", - "mistralai/mistral-7b-instruct", - "cognitivecomputations/dolphin-mixtral-8x22b", - "sao10k/l3-70b-euryale-v2.1", - "sophosympatheia/midnight-rose-70b", - "gryphe/mythomax-l2-13b", - "Nous-Hermes-2-Mixtral-8x7B-DPO", - "lzlv_70b", - "teknium/openhermes-2.5-mistral-7b", - "microsoft/wizardlm-2-8x22b", +var ModelList = []*model.ModelConfig{ + { + Model: "meta-llama/llama-3-8b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "meta-llama/llama-3-70b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "nousresearch/hermes-2-pro-llama-3-8b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "nousresearch/nous-hermes-llama2-13b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "mistralai/mistral-7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "teknium/openhermes-2.5-mistral-7b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "microsoft/wizardlm-2-8x22b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMicrosoft, + }, } diff --git a/service/aiproxy/relay/adaptor/novita/main.go b/service/aiproxy/relay/adaptor/novita/main.go deleted file mode 100644 index b33c100aed9..00000000000 --- a/service/aiproxy/relay/adaptor/novita/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package novita - -import ( - "fmt" - - "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" -) - -func GetRequestURL(meta *meta.Meta) (string, error) { - if meta.Mode == relaymode.ChatCompletions { - return meta.BaseURL + "/chat/completions", nil - } - return "", fmt.Errorf("unsupported relay mode %d for novita", meta.Mode) -} diff --git a/service/aiproxy/relay/adaptor/ollama/adaptor.go b/service/aiproxy/relay/adaptor/ollama/adaptor.go index 2c6f048f61e..d281190f726 100644 --- a/service/aiproxy/relay/adaptor/ollama/adaptor.go +++ b/service/aiproxy/relay/adaptor/ollama/adaptor.go @@ -2,84 +2,85 @@ package ollama import ( "errors" + "fmt" "io" "net/http" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) type Adaptor struct{} -func (a *Adaptor) Init(_ *meta.Meta) { -} +const baseURL = "http://localhost:11434" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { // https://github.com/ollama/ollama/blob/main/docs/api.md - fullRequestURL := meta.BaseURL + "/api/chat" - if meta.Mode == relaymode.Embeddings { - fullRequestURL = meta.BaseURL + "/api/embed" + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } + switch meta.Mode { + case relaymode.Embeddings: + return u + "/api/embed", nil + case relaymode.ChatCompletions: + return u + "/api/chat", nil + default: + return "", fmt.Errorf("unsupported mode: %d", meta.Mode) } - return fullRequestURL, nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+meta.Channel.Key) return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { if request == nil { - return nil, errors.New("request is nil") + return nil, nil, errors.New("request is nil") } - switch relayMode { + switch meta.Mode { case relaymode.Embeddings: - ollamaEmbeddingRequest := ConvertEmbeddingRequest(request) - return ollamaEmbeddingRequest, nil + return ConvertEmbeddingRequest(meta, request) + case relaymode.ChatCompletions: + return ConvertRequest(meta, request) default: - return ConvertRequest(request), nil - } -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") + return nil, nil, fmt.Errorf("unsupported mode: %d", meta.Mode) } - return request, nil } -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil +func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.Reader, error) { + return nil, errors.New("not implemented") } -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil +func (a *Adaptor) ConvertTTSRequest(*relaymodel.TextToSpeechRequest) (any, error) { + return nil, errors.New("not implemented") } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp) - } else { - switch meta.Mode { - case relaymode.Embeddings: - err, usage = EmbeddingHandler(c, resp) - default: +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.Embeddings: + err, usage = EmbeddingHandler(c, resp) + default: + if utils.IsStreamResponse(resp) { + err, usage = StreamHandler(c, resp) + } else { err, usage = Handler(c, resp) } } return } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/ollama/constants.go b/service/aiproxy/relay/adaptor/ollama/constants.go index d9dc72a8a51..739ec1f0a80 100644 --- a/service/aiproxy/relay/adaptor/ollama/constants.go +++ b/service/aiproxy/relay/adaptor/ollama/constants.go @@ -1,11 +1,44 @@ package ollama -var ModelList = []string{ - "codellama:7b-instruct", - "llama2:7b", - "llama2:latest", - "llama3:latest", - "phi3:latest", - "qwen:0.5b-chat", - "qwen:7b", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "codellama:7b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama2:7b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama2:latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "llama3:latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "phi3:latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMicrosoft, + }, + { + Model: "qwen:0.5b-chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, + { + Model: "qwen:7b", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, } diff --git a/service/aiproxy/relay/adaptor/ollama/main.go b/service/aiproxy/relay/adaptor/ollama/main.go index d3967f6bb2d..dc7a6c77011 100644 --- a/service/aiproxy/relay/adaptor/ollama/main.go +++ b/service/aiproxy/relay/adaptor/ollama/main.go @@ -2,12 +2,15 @@ package ollama import ( "bufio" + "bytes" + "io" "net/http" "strings" json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/conv" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/common/helper" "github.com/labring/sealos/service/aiproxy/common/random" @@ -15,13 +18,21 @@ import ( "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/image" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/constant" - "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) -func ConvertRequest(request *model.GeneralOpenAIRequest) *ChatRequest { +func ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + var request relaymodel.GeneralOpenAIRequest + err := common.UnmarshalBodyReusable(req, &request) + if err != nil { + return nil, nil, err + } + request.Model = meta.ActualModelName + ollamaRequest := ChatRequest{ Model: request.Model, Options: &Options{ @@ -41,10 +52,13 @@ func ConvertRequest(request *model.GeneralOpenAIRequest) *ChatRequest { var contentText string for _, part := range openaiContent { switch part.Type { - case model.ContentTypeText: + case relaymodel.ContentTypeText: contentText = part.Text - case model.ContentTypeImageURL: - _, data, _ := image.GetImageFromURL(part.ImageURL.URL) + case relaymodel.ContentTypeImageURL: + _, data, err := image.GetImageFromURL(req.Context(), part.ImageURL.URL) + if err != nil { + return nil, nil, err + } imageUrls = append(imageUrls, data) } } @@ -54,13 +68,19 @@ func ConvertRequest(request *model.GeneralOpenAIRequest) *ChatRequest { Images: imageUrls, }) } - return &ollamaRequest + + data, err := json.Marshal(ollamaRequest) + if err != nil { + return nil, nil, err + } + + return nil, bytes.NewReader(data), nil } func responseOllama2OpenAI(response *ChatResponse) *openai.TextResponse { choice := openai.TextResponseChoice{ Index: 0, - Message: model.Message{ + Message: relaymodel.Message{ Role: response.Message.Role, Content: response.Message.Content, }, @@ -73,8 +93,8 @@ func responseOllama2OpenAI(response *ChatResponse) *openai.TextResponse { Model: response.Model, Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, - Usage: model.Usage{ + Choices: []*openai.TextResponseChoice{&choice}, + Usage: relaymodel.Usage{ PromptTokens: response.PromptEvalCount, CompletionTokens: response.EvalCount, TotalTokens: response.PromptEvalCount + response.EvalCount, @@ -95,15 +115,17 @@ func streamResponseOllama2OpenAI(ollamaResponse *ChatResponse) *openai.ChatCompl Object: "chat.completion.chunk", Created: helper.GetTimestamp(), Model: ollamaResponse.Model, - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, + Choices: []*openai.ChatCompletionsStreamResponseChoice{&choice}, } return &response } -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { +func StreamHandler(c *gin.Context, resp *http.Response) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { defer resp.Body.Close() - var usage model.Usage + log := middleware.GetLogger(c) + + var usage relaymodel.Usage scanner := bufio.NewScanner(resp.Body) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { @@ -129,7 +151,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC var ollamaResponse ChatResponse err := json.Unmarshal(conv.StringToBytes(data), &ollamaResponse) if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) + log.Error("error unmarshalling stream response: " + err.Error()) continue } @@ -142,12 +164,12 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC response := streamResponseOllama2OpenAI(&ollamaResponse) err = render.ObjectData(c, response) if err != nil { - logger.SysError(err.Error()) + log.Error("error rendering stream response: " + err.Error()) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } render.Done(c) @@ -155,8 +177,13 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC return nil, &usage } -func ConvertEmbeddingRequest(request *model.GeneralOpenAIRequest) *EmbeddingRequest { - return &EmbeddingRequest{ +func ConvertEmbeddingRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + request, err := utils.UnmarshalGeneralOpenAIRequest(req) + if err != nil { + return nil, nil, err + } + request.Model = meta.ActualModelName + data, err := json.Marshal(&EmbeddingRequest{ Model: request.Model, Input: request.ParseInput(), Options: &Options{ @@ -166,10 +193,14 @@ func ConvertEmbeddingRequest(request *model.GeneralOpenAIRequest) *EmbeddingRequ FrequencyPenalty: request.FrequencyPenalty, PresencePenalty: request.PresencePenalty, }, + }) + if err != nil { + return nil, nil, err } + return nil, bytes.NewReader(data), nil } -func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { +func EmbeddingHandler(c *gin.Context, resp *http.Response) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { defer resp.Body.Close() var ollamaResponse EmbeddingResponse @@ -179,8 +210,8 @@ func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStat } if ollamaResponse.Error != "" { - return &model.ErrorWithStatusCode{ - Error: model.Error{ + return &relaymodel.ErrorWithStatusCode{ + Error: relaymodel.Error{ Message: ollamaResponse.Error, Type: "ollama_error", Param: "", @@ -204,13 +235,13 @@ func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStat func embeddingResponseOllama2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { openAIEmbeddingResponse := openai.EmbeddingResponse{ Object: "list", - Data: make([]openai.EmbeddingResponseItem, 0, 1), + Data: make([]*openai.EmbeddingResponseItem, 0, 1), Model: response.Model, - Usage: model.Usage{TotalTokens: 0}, + Usage: relaymodel.Usage{TotalTokens: 0}, } for i, embedding := range response.Embeddings { - openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, &openai.EmbeddingResponseItem{ Object: `embedding`, Index: i, Embedding: embedding, @@ -219,7 +250,7 @@ func embeddingResponseOllama2OpenAI(response *EmbeddingResponse) *openai.Embeddi return &openAIEmbeddingResponse } -func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { +func Handler(c *gin.Context, resp *http.Response) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { defer resp.Body.Close() var ollamaResponse ChatResponse @@ -228,8 +259,8 @@ func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, * return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil } if ollamaResponse.Error != "" { - return &model.ErrorWithStatusCode{ - Error: model.Error{ + return &relaymodel.ErrorWithStatusCode{ + Error: relaymodel.Error{ Message: ollamaResponse.Error, Type: "ollama_error", Param: "", diff --git a/service/aiproxy/relay/adaptor/openai/adaptor.go b/service/aiproxy/relay/adaptor/openai/adaptor.go index a5cab79b89b..e4d3a5bd370 100644 --- a/service/aiproxy/relay/adaptor/openai/adaptor.go +++ b/service/aiproxy/relay/adaptor/openai/adaptor.go @@ -5,209 +5,194 @@ import ( "errors" "fmt" "io" - "mime/multipart" "net/http" "strings" "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/doubao" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/minimax" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/novita" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) -type Adaptor struct { - meta *meta.Meta - contentType string - responseFormat string -} +var _ adaptor.Adaptor = (*Adaptor)(nil) -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} +type Adaptor struct{} + +const baseURL = "https://api.openai.com" + +const MetaBaseURLNoV1 = "base_url_no_v1" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - switch meta.ChannelType { - case channeltype.Azure: - switch meta.Mode { - case relaymode.ImagesGenerations: - // https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api - // https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2024-03-01-preview - return fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", meta.BaseURL, meta.ActualModelName, meta.Config.APIVersion), nil - case relaymode.AudioTranscription: - // https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api - return fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", meta.BaseURL, meta.ActualModelName, meta.Config.APIVersion), nil - case relaymode.AudioSpeech: - // https://learn.microsoft.com/en-us/azure/ai-services/openai/text-to-speech-quickstart?tabs=command-line#rest-api - return fmt.Sprintf("%s/openai/deployments/%s/audio/speech?api-version=%s", meta.BaseURL, meta.ActualModelName, meta.Config.APIVersion), nil - } + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } - // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api - requestURL := strings.Split(meta.RequestURLPath, "?")[0] - requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, meta.Config.APIVersion) - task := strings.TrimPrefix(requestURL, "/v1/") - model := strings.ReplaceAll(meta.ActualModelName, ".", "") - // https://github.com/labring/sealos/service/aiproxy/issues/1191 - // {your endpoint}/openai/deployments/{your azure_model}/chat/completions?api-version={api_version} - requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model, task) - return GetFullRequestURL(meta.BaseURL, requestURL, meta.ChannelType), nil - case channeltype.Minimax: - return minimax.GetRequestURL(meta) - case channeltype.Doubao: - return doubao.GetRequestURL(meta) - case channeltype.Novita: - return novita.GetRequestURL(meta) + var path string + switch meta.Mode { + case relaymode.ChatCompletions: + path = "/chat/completions" + case relaymode.Completions: + path = "/completions" + case relaymode.Embeddings: + path = "/embeddings" + case relaymode.Moderations: + path = "/moderations" + case relaymode.ImagesGenerations: + path = "/images/generations" + case relaymode.Edits: + path = "/edits" + case relaymode.AudioSpeech: + path = "/audio/speech" + case relaymode.AudioTranscription: + path = "/audio/transcriptions" + case relaymode.AudioTranslation: + path = "/audio/translations" + case relaymode.Rerank: + path = "/rerank" default: - return GetFullRequestURL(meta.BaseURL, meta.RequestURLPath, meta.ChannelType), nil + return "", errors.New("unsupported mode") } -} -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - if meta.ChannelType == channeltype.Azure { - req.Header.Set("Api-Key", meta.APIKey) - return nil - } - if a.contentType != "" { - req.Header.Set("Content-Type", a.contentType) - } - req.Header.Set("Authorization", "Bearer "+meta.APIKey) - if meta.ChannelType == channeltype.OpenRouter { - req.Header.Set("Http-Referer", "https://github.com/labring/sealos/service/aiproxy") - req.Header.Set("X-Title", "One API") + if meta.GetBool(MetaBaseURLNoV1) || + (strings.HasPrefix(u, "https://gateway.ai.cloudflare.com") && strings.HasSuffix(u, "/openai")) { + return u + path, nil } + return fmt.Sprintf("%s/v1%s", u, path), nil +} + +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+meta.Channel.Key) return nil } -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - if request.Stream { - // always return usage in stream mode - if request.StreamOptions == nil { - request.StreamOptions = &model.StreamOptions{} - } - request.StreamOptions.IncludeUsage = true - } - return request, nil +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + return ConvertRequest(meta, req) } -func (a *Adaptor) ConvertTTSRequest(request *model.TextToSpeechRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + if req == nil { + return nil, nil, errors.New("request is nil") } - if len(request.Input) > 4096 { - return nil, errors.New("input is too long (over 4096 characters)") + switch meta.Mode { + case relaymode.Moderations: + meta.Set(MetaEmbeddingsPatchInputToSlices, true) + return ConvertEmbeddingsRequest(meta, req) + case relaymode.Embeddings: + return ConvertEmbeddingsRequest(meta, req) + case relaymode.ChatCompletions: + return ConvertTextRequest(meta, req) + case relaymode.ImagesGenerations: + return ConvertImageRequest(meta, req) + case relaymode.AudioTranscription, relaymode.AudioTranslation: + return ConvertSTTRequest(meta, req) + case relaymode.AudioSpeech: + return ConvertTTSRequest(meta, req) + case relaymode.Rerank: + return ConvertRerankRequest(meta, req) + default: + return nil, nil, errors.New("unsupported convert request mode") } - return request, nil } -func (a *Adaptor) ConvertSTTRequest(request *http.Request) (io.ReadCloser, error) { - if request == nil { - return nil, errors.New("request is nil") +func DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.ImagesGenerations: + usage, err = ImageHandler(meta, c, resp) + case relaymode.AudioTranscription, relaymode.AudioTranslation: + usage, err = STTHandler(meta, c, resp) + case relaymode.AudioSpeech: + usage, err = TTSHandler(meta, c, resp) + case relaymode.Rerank: + usage, err = RerankHandler(meta, c, resp) + case relaymode.Moderations: + usage, err = ModerationsHandler(meta, c, resp) + case relaymode.Embeddings: + fallthrough + case relaymode.ChatCompletions: + if utils.IsStreamResponse(resp) { + usage, err = StreamHandler(meta, c, resp) + } else { + usage, err = Handler(meta, c, resp) + } + default: + return nil, ErrorWrapperWithMessage("unsupported response mode", "unsupported_mode", http.StatusBadRequest) } + return +} + +const DoNotPatchStreamOptionsIncludeUsageMetaKey = "do_not_patch_stream_options_include_usage" - err := request.ParseMultipartForm(1024 * 1024 * 4) +func ConvertTextRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + reqMap := make(map[string]any) + err := common.UnmarshalBodyReusable(req, &reqMap) if err != nil { - return nil, err + return nil, nil, err } - multipartBody := &bytes.Buffer{} - multipartWriter := multipart.NewWriter(multipartBody) - - for key, values := range request.MultipartForm.Value { - for _, value := range values { - if key == "model" { - err = multipartWriter.WriteField(key, a.meta.ActualModelName) - if err != nil { - return nil, err - } - continue - } - if key == "response_format" { - a.responseFormat = value - } - err = multipartWriter.WriteField(key, value) - if err != nil { - return nil, err - } + if !meta.GetBool(DoNotPatchStreamOptionsIncludeUsageMetaKey) { + if err := patchStreamOptions(reqMap); err != nil { + return nil, nil, err } } - for key, files := range request.MultipartForm.File { - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - return nil, err - } - w, err := multipartWriter.CreateFormFile(key, fileHeader.Filename) - if err != nil { - file.Close() - return nil, err - } - _, err = io.Copy(w, file) - file.Close() - if err != nil { - return nil, err - } - } + reqMap["model"] = meta.ActualModelName + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err } - - multipartWriter.Close() - a.contentType = multipartWriter.FormDataContentType() - return io.NopCloser(multipartBody), nil + return nil, bytes.NewReader(jsonData), nil } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") +func patchStreamOptions(reqMap map[string]any) error { + stream, ok := reqMap["stream"] + if !ok { + return nil } - return request, nil -} -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} + streamBool, ok := stream.(bool) + if !ok { + return errors.New("stream is not a boolean") + } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - var responseText string - err, responseText, usage = StreamHandler(c, resp, meta.Mode) - if usage == nil || usage.TotalTokens == 0 { - usage = ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) + if !streamBool { + return nil + } + + streamOptions, ok := reqMap["stream_options"].(map[string]any) + if !ok { + if reqMap["stream_options"] != nil { + return errors.New("stream_options is not a map") } - if usage.TotalTokens != 0 && usage.PromptTokens == 0 { // some channels don't return prompt tokens & completion tokens - usage.PromptTokens = meta.PromptTokens - usage.CompletionTokens = usage.TotalTokens - meta.PromptTokens + reqMap["stream_options"] = map[string]any{ + "include_usage": true, } - return - } - switch meta.Mode { - case relaymode.ImagesGenerations: - err, _ = ImageHandler(c, resp) - case relaymode.AudioTranscription: - err, usage = STTHandler(c, resp, meta, a.responseFormat) - case relaymode.AudioSpeech: - err, usage = TTSHandler(c, resp, meta) - case relaymode.Rerank: - err, usage = RerankHandler(c, resp, meta.PromptTokens, meta) - default: - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + return nil } - return + + streamOptions["include_usage"] = true + return nil +} + +const MetaResponseFormat = "response_format" + +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) +} + +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + return DoResponse(meta, c, resp) } -func (a *Adaptor) GetModelList() []string { - _, modelList := GetCompatibleChannelMeta(a.meta.ChannelType) - return modelList +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList } func (a *Adaptor) GetChannelName() string { - channelName, _ := GetCompatibleChannelMeta(a.meta.ChannelType) - return channelName + return "openai" } diff --git a/service/aiproxy/relay/adaptor/openai/balance.go b/service/aiproxy/relay/adaptor/openai/balance.go new file mode 100644 index 00000000000..e6f7efee34f --- /dev/null +++ b/service/aiproxy/relay/adaptor/openai/balance.go @@ -0,0 +1,65 @@ +package openai + +import ( + "context" + "net/http" + "time" + + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common/client" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" +) + +var _ adaptor.GetBalance = (*Adaptor)(nil) + +func (a *Adaptor) GetBalance(channel *model.Channel) (float64, error) { + return GetBalance(channel) +} + +func GetBalance(channel *model.Channel) (float64, error) { + u := channel.BaseURL + if u == "" { + u = baseURL + } + url := u + "/v1/dashboard/billing/subscription" + + req1, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return 0, err + } + req1.Header.Set("Authorization", "Bearer "+channel.Key) + res1, err := client.HTTPClient.Do(req1) + if err != nil { + return 0, err + } + defer res1.Body.Close() + subscription := SubscriptionResponse{} + err = json.NewDecoder(res1.Body).Decode(&subscription) + if err != nil { + return 0, err + } + now := time.Now() + startDate := now.Format("2006-01") + "-01" + endDate := now.Format("2006-01-02") + if !subscription.HasPaymentMethod { + startDate = now.AddDate(0, 0, -100).Format("2006-01-02") + } + url = u + "/v1/dashboard/billing/usage?start_date=" + startDate + "&end_date=" + endDate + req2, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return 0, err + } + req2.Header.Set("Authorization", "Bearer "+channel.Key) + res2, err := client.HTTPClient.Do(req2) + if err != nil { + return 0, err + } + usage := UsageResponse{} + err = json.NewDecoder(res2.Body).Decode(&usage) + if err != nil { + return 0, err + } + balance := subscription.HardLimitUSD - usage.TotalUsage/100 + return balance, nil +} diff --git a/service/aiproxy/relay/adaptor/openai/compatible.go b/service/aiproxy/relay/adaptor/openai/compatible.go deleted file mode 100644 index 401488ddc96..00000000000 --- a/service/aiproxy/relay/adaptor/openai/compatible.go +++ /dev/null @@ -1,70 +0,0 @@ -package openai - -import ( - "github.com/labring/sealos/service/aiproxy/relay/adaptor/ai360" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/baichuan" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/deepseek" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/doubao" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/groq" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/lingyiwanwu" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/minimax" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/mistral" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/moonshot" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/novita" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/siliconflow" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/stepfun" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/togetherai" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" -) - -var CompatibleChannels = []int{ - channeltype.Azure, - channeltype.AI360, - channeltype.Moonshot, - channeltype.Baichuan, - channeltype.Minimax, - channeltype.Doubao, - channeltype.Mistral, - channeltype.Groq, - channeltype.LingYiWanWu, - channeltype.StepFun, - channeltype.DeepSeek, - channeltype.TogetherAI, - channeltype.Novita, - channeltype.SiliconFlow, -} - -func GetCompatibleChannelMeta(channelType int) (string, []string) { - switch channelType { - case channeltype.Azure: - return "azure", ModelList - case channeltype.AI360: - return "360", ai360.ModelList - case channeltype.Moonshot: - return "moonshot", moonshot.ModelList - case channeltype.Baichuan: - return "baichuan", baichuan.ModelList - case channeltype.Minimax: - return "minimax", minimax.ModelList - case channeltype.Mistral: - return "mistralai", mistral.ModelList - case channeltype.Groq: - return "groq", groq.ModelList - case channeltype.LingYiWanWu: - return "lingyiwanwu", lingyiwanwu.ModelList - case channeltype.StepFun: - return "stepfun", stepfun.ModelList - case channeltype.DeepSeek: - return "deepseek", deepseek.ModelList - case channeltype.TogetherAI: - return "together.ai", togetherai.ModelList - case channeltype.Doubao: - return "doubao", doubao.ModelList - case channeltype.Novita: - return "novita", novita.ModelList - case channeltype.SiliconFlow: - return "siliconflow", siliconflow.ModelList - default: - return "openai", ModelList - } -} diff --git a/service/aiproxy/relay/adaptor/openai/constants.go b/service/aiproxy/relay/adaptor/openai/constants.go index aacdba1ad3e..bbdb1f728f1 100644 --- a/service/aiproxy/relay/adaptor/openai/constants.go +++ b/service/aiproxy/relay/adaptor/openai/constants.go @@ -1,23 +1,267 @@ package openai -var ModelList = []string{ - "gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125", - "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-instruct", - "gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview", - "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", - "gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", - "gpt-4o", "gpt-4o-2024-05-13", - "gpt-4o-2024-08-06", - "chatgpt-4o-latest", - "gpt-4o-mini", "gpt-4o-mini-2024-07-18", - "gpt-4-vision-preview", - "text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large", - "text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003", - "text-moderation-latest", "text-moderation-stable", - "text-davinci-edit-001", - "davinci-002", "babbage-002", - "dall-e-2", "dall-e-3", - "whisper-1", - "tts-1", "tts-1-1106", "tts-1-hd", "tts-1-hd-1106", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "gpt-3.5-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.022, + OutputPrice: 0.044, + }, + { + Model: "gpt-3.5-turbo-0301", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-3.5-turbo-0613", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-3.5-turbo-1106", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-3.5-turbo-0125", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-3.5-turbo-16k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.022, + OutputPrice: 0.044, + }, + { + Model: "gpt-3.5-turbo-16k-0613", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-3.5-turbo-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.22, + OutputPrice: 0.44, + }, + { + Model: "gpt-4-0314", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-0613", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-1106-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-0125-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.44, + OutputPrice: 0.88, + }, + { + Model: "gpt-4-32k-0314", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-32k-0613", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-turbo-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.071, + OutputPrice: 0.213, + }, + { + Model: "gpt-4-turbo-2024-04-09", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4o", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.01775, + OutputPrice: 0.071, + }, + { + Model: "gpt-4o-2024-05-13", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4o-2024-08-06", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "chatgpt-4o-latest", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4o-mini", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.001065, + OutputPrice: 0.00426, + }, + { + Model: "gpt-4o-mini-2024-07-18", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "gpt-4-vision-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "o1-mini", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.0213, + OutputPrice: 0.0852, + }, + { + Model: "o1-preview", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + InputPrice: 0.1065, + OutputPrice: 0.426, + }, + { + Model: "text-embedding-ada-002", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-embedding-3-small", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-embedding-3-large", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-curie-001", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-babbage-001", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-ada-001", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-davinci-002", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-davinci-003", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-moderation-latest", + Type: relaymode.Moderations, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-moderation-stable", + Type: relaymode.Moderations, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "text-davinci-edit-001", + Type: relaymode.Edits, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "davinci-002", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "babbage-002", + Type: relaymode.Completions, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "dall-e-2", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "dall-e-3", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "whisper-1", + Type: relaymode.AudioTranscription, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "tts-1", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "tts-1-1106", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "tts-1-hd", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerOpenAI, + }, + { + Model: "tts-1-hd-1106", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerOpenAI, + }, } diff --git a/service/aiproxy/relay/adaptor/openai/embeddings.go b/service/aiproxy/relay/adaptor/openai/embeddings.go new file mode 100644 index 00000000000..d0c2997682e --- /dev/null +++ b/service/aiproxy/relay/adaptor/openai/embeddings.go @@ -0,0 +1,37 @@ +package openai + +import ( + "bytes" + "io" + "net/http" + + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +const MetaEmbeddingsPatchInputToSlices = "embeddings_input_to_slices" + +//nolint:gocritic +func ConvertEmbeddingsRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + reqMap := make(map[string]any) + err := common.UnmarshalBodyReusable(req, &reqMap) + if err != nil { + return nil, nil, err + } + + reqMap["model"] = meta.ActualModelName + + if meta.GetBool(MetaEmbeddingsPatchInputToSlices) { + switch v := reqMap["input"].(type) { + case string: + reqMap["input"] = []string{v} + } + } + + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(jsonData), nil +} diff --git a/service/aiproxy/relay/adaptor/openai/helper.go b/service/aiproxy/relay/adaptor/openai/helper.go index 4ba22af5b09..1c4b1f5e40d 100644 --- a/service/aiproxy/relay/adaptor/openai/helper.go +++ b/service/aiproxy/relay/adaptor/openai/helper.go @@ -4,28 +4,23 @@ import ( "fmt" "strings" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" "github.com/labring/sealos/service/aiproxy/relay/model" ) func ResponseText2Usage(responseText string, modeName string, promptTokens int) *model.Usage { - usage := &model.Usage{} - usage.PromptTokens = promptTokens - usage.CompletionTokens = CountTokenText(responseText, modeName) + usage := &model.Usage{ + PromptTokens: promptTokens, + CompletionTokens: CountTokenText(responseText, modeName), + } usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens return usage } -func GetFullRequestURL(baseURL string, requestURL string, channelType int) string { +func GetFullRequestURL(baseURL string, requestURL string) string { fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") { - switch channelType { - case channeltype.OpenAI: - fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/v1")) - case channeltype.Azure: - fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments")) - } + fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments")) } return fullRequestURL } diff --git a/service/aiproxy/relay/adaptor/openai/image.go b/service/aiproxy/relay/adaptor/openai/image.go index d52435fdba2..9c2ea8d8f97 100644 --- a/service/aiproxy/relay/adaptor/openai/image.go +++ b/service/aiproxy/relay/adaptor/openai/image.go @@ -7,38 +7,71 @@ import ( "github.com/gin-gonic/gin" json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/common/image" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" ) -func ImageHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - var imageResponse ImageResponse - responseBody, err := io.ReadAll(resp.Body) +func ConvertImageRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + reqMap := make(map[string]any) + err := common.UnmarshalBodyReusable(req, &reqMap) if err != nil { - return ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + return nil, nil, err } - err = resp.Body.Close() + meta.Set(MetaResponseFormat, reqMap["response_format"]) + + reqMap["model"] = meta.ActualModelName + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(jsonData), nil +} + +func ImageHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + responseFormat := meta.GetString(MetaResponseFormat) + + responseBody, err := io.ReadAll(resp.Body) if err != nil { - return ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + return nil, ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) } + var imageResponse ImageResponse err = json.Unmarshal(responseBody, &imageResponse) if err != nil { - return ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + return nil, ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } - resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) + usage := &model.Usage{ + PromptTokens: len(imageResponse.Data), + TotalTokens: len(imageResponse.Data), + } - for k, v := range resp.Header { - c.Writer.Header().Set(k, v[0]) + if responseFormat == "b64_json" { + for _, data := range imageResponse.Data { + if len(data.B64Json) > 0 { + continue + } + _, data.B64Json, err = image.GetImageFromURL(c.Request.Context(), data.URL) + if err != nil { + return usage, ErrorWrapper(err, "get_image_from_url_failed", http.StatusInternalServerError) + } + } } - c.Writer.WriteHeader(resp.StatusCode) - _, err = io.Copy(c.Writer, resp.Body) + data, err := json.Marshal(imageResponse) if err != nil { - return ErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil + return usage, ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) } - err = resp.Body.Close() + + _, err = c.Writer.Write(data) if err != nil { - return ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + log.Error("write response body failed: " + err.Error()) } - return nil, nil + return usage, nil } diff --git a/service/aiproxy/relay/adaptor/openai/main.go b/service/aiproxy/relay/adaptor/openai/main.go index a90d8ff8e99..bb670ee6a76 100644 --- a/service/aiproxy/relay/adaptor/openai/main.go +++ b/service/aiproxy/relay/adaptor/openai/main.go @@ -2,8 +2,6 @@ package openai import ( "bufio" - "bytes" - "fmt" "io" "net/http" "strings" @@ -11,268 +9,167 @@ import ( json "github.com/json-iterator/go" "github.com/labring/sealos/service/aiproxy/common/render" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) const ( - dataPrefix = "data: " - done = "[DONE]" - dataPrefixLength = len(dataPrefix) + DataPrefix = "data: " + Done = "[DONE]" + DataPrefixLength = len(DataPrefix) ) -func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.ErrorWithStatusCode, string, *model.Usage) { +var stdjson = json.ConfigCompatibleWithStandardLibrary + +type UsageAndChoicesResponse struct { + Usage *model.Usage + Choices []*ChatCompletionsStreamResponseChoice +} + +func StreamHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { defer resp.Body.Close() + log := middleware.GetLogger(c) + responseText := "" scanner := bufio.NewScanner(resp.Body) scanner.Split(bufio.ScanLines) + var usage *model.Usage common.SetEventStreamHeaders(c) - doneRendered := false for scanner.Scan() { data := scanner.Text() - if len(data) < dataPrefixLength { // ignore blank line or wrong format + if len(data) < DataPrefixLength { // ignore blank line or wrong format continue } - if data[:dataPrefixLength] != dataPrefix && data[:dataPrefixLength] != done { + if data[:DataPrefixLength] != DataPrefix { continue } - if strings.HasPrefix(data[dataPrefixLength:], done) { - render.StringData(c, data) - doneRendered = true - continue + data = data[DataPrefixLength:] + if strings.HasPrefix(data, Done) { + break } - switch relayMode { + switch meta.Mode { case relaymode.ChatCompletions: - var streamResponse ChatCompletionsStreamResponse - err := json.Unmarshal(conv.StringToBytes(data[dataPrefixLength:]), &streamResponse) + var streamResponse UsageAndChoicesResponse + err := json.Unmarshal(conv.StringToBytes(data), &streamResponse) if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) - render.StringData(c, data) // if error happened, pass the data to client - continue // just ignore the error + log.Error("error unmarshalling stream response: " + err.Error()) + continue // just ignore the error } if len(streamResponse.Choices) == 0 && streamResponse.Usage == nil { // but for empty choice and no usage, we should not pass it to client, this is for azure continue // just ignore empty choice } - render.StringData(c, data) - for _, choice := range streamResponse.Choices { - responseText += conv.AsString(choice.Delta.Content) - } if streamResponse.Usage != nil { usage = streamResponse.Usage } + for _, choice := range streamResponse.Choices { + responseText += choice.Delta.StringContent() + } + // streamResponse.Model = meta.ActualModelName + respMap := make(map[string]any) + err = json.Unmarshal(conv.StringToBytes(data), &respMap) + if err != nil { + log.Error("error unmarshalling stream response: " + err.Error()) + continue + } + if _, ok := respMap["model"]; ok && meta.OriginModelName != "" { + respMap["model"] = meta.OriginModelName + } + err = render.ObjectData(c, respMap) + if err != nil { + log.Error("error rendering stream response: " + err.Error()) + continue + } case relaymode.Completions: - render.StringData(c, data) var streamResponse CompletionsStreamResponse - err := json.Unmarshal(conv.StringToBytes(data[dataPrefixLength:]), &streamResponse) + err := json.Unmarshal(conv.StringToBytes(data), &streamResponse) if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) + log.Error("error unmarshalling stream response: " + err.Error()) continue } for _, choice := range streamResponse.Choices { responseText += choice.Text } + render.StringData(c, data) } } if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) + log.Error("error reading stream: " + err.Error()) } - if !doneRendered { - render.Done(c) + render.Done(c) + + if usage == nil || (usage.TotalTokens == 0 && responseText != "") { + usage = ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) } - return nil, responseText, usage + if usage.TotalTokens != 0 && usage.PromptTokens == 0 { // some channels don't return prompt tokens & completion tokens + usage.PromptTokens = meta.PromptTokens + usage.CompletionTokens = usage.TotalTokens - meta.PromptTokens + } + + return usage, nil } -func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { +func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { defer resp.Body.Close() + log := middleware.GetLogger(c) + responseBody, err := io.ReadAll(resp.Body) if err != nil { - return ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + return nil, ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) } var textResponse SlimTextResponse err = json.Unmarshal(responseBody, &textResponse) if err != nil { - return ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + return nil, ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } if textResponse.Error.Type != "" { - return &model.ErrorWithStatusCode{ - Error: textResponse.Error, - StatusCode: resp.StatusCode, - }, nil + return nil, ErrorWrapperWithMessage(textResponse.Error.Message, textResponse.Error.Code, http.StatusBadRequest) } if textResponse.Usage.TotalTokens == 0 || (textResponse.Usage.PromptTokens == 0 && textResponse.Usage.CompletionTokens == 0) { completionTokens := 0 for _, choice := range textResponse.Choices { - completionTokens += CountTokenText(choice.Message.StringContent(), modelName) + completionTokens += CountTokenText(choice.Message.StringContent(), meta.ActualModelName) } textResponse.Usage = model.Usage{ - PromptTokens: promptTokens, + PromptTokens: meta.PromptTokens, CompletionTokens: completionTokens, - TotalTokens: promptTokens + completionTokens, } } + textResponse.Usage.TotalTokens = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens - for k, v := range resp.Header { - c.Writer.Header().Set(k, v[0]) - } - c.Writer.WriteHeader(resp.StatusCode) - - _, _ = c.Writer.Write(responseBody) - return nil, &textResponse.Usage -} - -func RerankHandler(c *gin.Context, resp *http.Response, promptTokens int, _ *meta.Meta) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - responseBody, err := io.ReadAll(resp.Body) + var respMap map[string]any + err = json.Unmarshal(responseBody, &respMap) if err != nil { - return ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + return &textResponse.Usage, ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } - var rerankResponse SlimRerankResponse - err = json.Unmarshal(responseBody, &rerankResponse) - if err != nil { - return ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(responseBody) - - if rerankResponse.Meta.Tokens == nil { - return nil, &model.Usage{ - PromptTokens: promptTokens, - CompletionTokens: 0, - TotalTokens: promptTokens, - } - } - if rerankResponse.Meta.Tokens.InputTokens <= 0 { - rerankResponse.Meta.Tokens.InputTokens = promptTokens - } - return nil, &model.Usage{ - PromptTokens: rerankResponse.Meta.Tokens.InputTokens, - CompletionTokens: rerankResponse.Meta.Tokens.OutputTokens, - TotalTokens: rerankResponse.Meta.Tokens.InputTokens + rerankResponse.Meta.Tokens.OutputTokens, + if _, ok := respMap["model"]; ok && meta.OriginModelName != "" { + respMap["model"] = meta.OriginModelName } -} - -func TTSHandler(c *gin.Context, resp *http.Response, meta *meta.Meta) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - for k, v := range resp.Header { - c.Writer.Header().Set(k, v[0]) - } - - _, _ = io.Copy(c.Writer, resp.Body) - return nil, &model.Usage{ - PromptTokens: meta.PromptTokens, - CompletionTokens: 0, - TotalTokens: meta.PromptTokens, - } -} - -func STTHandler(c *gin.Context, resp *http.Response, meta *meta.Meta, responseFormat string) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - responseBody, err := io.ReadAll(resp.Body) + newData, err := stdjson.Marshal(respMap) if err != nil { - return ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + return &textResponse.Usage, ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) } - var openAIErr SlimTextResponse - if err = json.Unmarshal(responseBody, &openAIErr); err == nil { - if openAIErr.Error.Message != "" { - return ErrorWrapper(fmt.Errorf("type %s, code %v, message %s", openAIErr.Error.Type, openAIErr.Error.Code, openAIErr.Error.Message), "request_error", http.StatusInternalServerError), nil - } - } - - var text string - switch responseFormat { - case "text": - text = getTextFromText(responseBody) - case "srt": - text, err = getTextFromSRT(responseBody) - case "verbose_json": - text, err = getTextFromVerboseJSON(responseBody) - case "vtt": - text, err = getTextFromVTT(responseBody) - case "json": - fallthrough - default: - text, err = getTextFromJSON(responseBody) - } + _, err = c.Writer.Write(newData) if err != nil { - return ErrorWrapper(err, "get_text_from_body_err", http.StatusInternalServerError), nil - } - completionTokens := CountTokenText(text, meta.ActualModelName) - - for k, v := range resp.Header { - c.Writer.Header().Set(k, v[0]) - } - _, _ = c.Writer.Write(responseBody) - - return nil, &model.Usage{ - PromptTokens: 0, - CompletionTokens: completionTokens, - TotalTokens: completionTokens, - } -} - -func getTextFromVTT(body []byte) (string, error) { - return getTextFromSRT(body) -} - -func getTextFromVerboseJSON(body []byte) (string, error) { - var whisperResponse WhisperVerboseJSONResponse - if err := json.Unmarshal(body, &whisperResponse); err != nil { - return "", fmt.Errorf("unmarshal_response_body_failed err :%w", err) - } - return whisperResponse.Text, nil -} - -func getTextFromSRT(body []byte) (string, error) { - scanner := bufio.NewScanner(bytes.NewReader(body)) - var builder strings.Builder - var textLine bool - for scanner.Scan() { - line := scanner.Text() - if textLine { - builder.WriteString(line) - textLine = false - continue - } else if strings.Contains(line, "-->") { - textLine = true - continue - } - } - if err := scanner.Err(); err != nil { - return "", err - } - return builder.String(), nil -} - -func getTextFromText(body []byte) string { - return strings.TrimSuffix(conv.BytesToString(body), "\n") -} - -func getTextFromJSON(body []byte) (string, error) { - var whisperResponse WhisperJSONResponse - if err := json.Unmarshal(body, &whisperResponse); err != nil { - return "", fmt.Errorf("unmarshal_response_body_failed err :%w", err) + log.Error("write response body failed: " + err.Error()) } - return whisperResponse.Text, nil + return &textResponse.Usage, nil } diff --git a/service/aiproxy/relay/adaptor/openai/model.go b/service/aiproxy/relay/adaptor/openai/model.go index fe8123b68d1..6c101b93398 100644 --- a/service/aiproxy/relay/adaptor/openai/model.go +++ b/service/aiproxy/relay/adaptor/openai/model.go @@ -13,16 +13,16 @@ type ImageContent struct { } type ChatRequest struct { - Model string `json:"model"` - Messages []model.Message `json:"messages"` - MaxTokens int `json:"max_tokens"` + Model string `json:"model"` + Messages []*model.Message `json:"messages"` + MaxTokens int `json:"max_tokens"` } type TextRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt"` - Messages []model.Message `json:"messages"` - MaxTokens int `json:"max_tokens"` + Model string `json:"model"` + Prompt string `json:"prompt"` + Messages []*model.Message `json:"messages"` + MaxTokens int `json:"max_tokens"` } // ImageRequest docs: https://platform.openai.com/docs/api-reference/images/create @@ -42,11 +42,11 @@ type WhisperJSONResponse struct { } type WhisperVerboseJSONResponse struct { - Task string `json:"task,omitempty"` - Language string `json:"language,omitempty"` - Text string `json:"text,omitempty"` - Segments []Segment `json:"segments,omitempty"` - Duration float64 `json:"duration,omitempty"` + Task string `json:"task,omitempty"` + Language string `json:"language,omitempty"` + Text string `json:"text,omitempty"` + Segments []*Segment `json:"segments,omitempty"` + Duration float64 `json:"duration,omitempty"` } type Segment struct { @@ -68,9 +68,9 @@ type UsageOrResponseText struct { } type SlimTextResponse struct { - Error model.Error `json:"error"` - Choices []TextResponseChoice `json:"choices"` - model.Usage `json:"usage"` + Error model.Error `json:"error"` + Choices []*TextResponseChoice `json:"choices"` + Usage model.Usage `json:"usage"` } type SlimRerankResponse struct { @@ -78,16 +78,16 @@ type SlimRerankResponse struct { } type TextResponseChoice struct { - FinishReason string `json:"finish_reason"` - model.Message `json:"message"` - Index int `json:"index"` + FinishReason string `json:"finish_reason"` + Message model.Message `json:"message"` + Index int `json:"index"` } type TextResponse struct { - ID string `json:"id"` - Model string `json:"model,omitempty"` - Object string `json:"object"` - Choices []TextResponseChoice `json:"choices"` + ID string `json:"id"` + Model string `json:"model,omitempty"` + Object string `json:"object"` + Choices []*TextResponseChoice `json:"choices"` model.Usage `json:"usage"` Created int64 `json:"created"` } @@ -99,9 +99,9 @@ type EmbeddingResponseItem struct { } type EmbeddingResponse struct { - Object string `json:"object"` - Model string `json:"model"` - Data []EmbeddingResponseItem `json:"data"` + Object string `json:"object"` + Model string `json:"model"` + Data []*EmbeddingResponseItem `json:"data"` model.Usage `json:"usage"` } @@ -112,8 +112,8 @@ type ImageData struct { } type ImageResponse struct { - Data []ImageData `json:"data"` - Created int64 `json:"created"` + Data []*ImageData `json:"data"` + Created int64 `json:"created"` } type ChatCompletionsStreamResponseChoice struct { @@ -123,17 +123,36 @@ type ChatCompletionsStreamResponseChoice struct { } type ChatCompletionsStreamResponse struct { - Usage *model.Usage `json:"usage,omitempty"` - ID string `json:"id"` - Object string `json:"object"` - Model string `json:"model"` - Choices []ChatCompletionsStreamResponseChoice `json:"choices"` - Created int64 `json:"created"` + Usage *model.Usage `json:"usage,omitempty"` + ID string `json:"id"` + Object string `json:"object"` + Model string `json:"model"` + Choices []*ChatCompletionsStreamResponseChoice `json:"choices"` + Created int64 `json:"created"` } type CompletionsStreamResponse struct { - Choices []struct { + Choices []*struct { Text string `json:"text"` FinishReason string `json:"finish_reason"` } `json:"choices"` } + +type SubscriptionResponse struct { + Object string `json:"object"` + HasPaymentMethod bool `json:"has_payment_method"` + SoftLimitUSD float64 `json:"soft_limit_usd"` + HardLimitUSD float64 `json:"hard_limit_usd"` + SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` + AccessUntil int64 `json:"access_until"` +} + +type UsageResponse struct { + Object string `json:"object"` + // DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"` + TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar +} + +type ErrorResp struct { + Error model.Error `json:"error"` +} diff --git a/service/aiproxy/relay/adaptor/openai/moderations.go b/service/aiproxy/relay/adaptor/openai/moderations.go new file mode 100644 index 00000000000..877d378f99c --- /dev/null +++ b/service/aiproxy/relay/adaptor/openai/moderations.go @@ -0,0 +1,58 @@ +package openai + +import ( + "io" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/model" +) + +func ModerationsHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + + var respMap map[string]any + err = json.Unmarshal(body, &respMap) + if err != nil { + return nil, ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + + if _, ok := respMap["error"]; ok { + var errorResp ErrorResp + err = json.Unmarshal(body, &errorResp) + if err != nil { + return nil, ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + return nil, ErrorWrapperWithMessage(errorResp.Error.Message, errorResp.Error.Code, http.StatusBadRequest) + } + + if _, ok := respMap["model"]; ok && meta.OriginModelName != "" { + respMap["model"] = meta.OriginModelName + } + + usage := &model.Usage{ + PromptTokens: meta.PromptTokens, + TotalTokens: meta.PromptTokens, + } + + newData, err := stdjson.Marshal(respMap) + if err != nil { + return usage, ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + + _, err = c.Writer.Write(newData) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return usage, nil +} diff --git a/service/aiproxy/relay/adaptor/openai/rerank.go b/service/aiproxy/relay/adaptor/openai/rerank.go new file mode 100644 index 00000000000..18f2cd04e64 --- /dev/null +++ b/service/aiproxy/relay/adaptor/openai/rerank.go @@ -0,0 +1,67 @@ +package openai + +import ( + "bytes" + "io" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/model" +) + +func ConvertRerankRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + reqMap := make(map[string]any) + err := common.UnmarshalBodyReusable(req, &reqMap) + if err != nil { + return nil, nil, err + } + reqMap["model"] = meta.ActualModelName + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(jsonData), nil +} + +func RerankHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + var rerankResponse SlimRerankResponse + err = json.Unmarshal(responseBody, &rerankResponse) + if err != nil { + return nil, ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + + c.Writer.WriteHeader(resp.StatusCode) + + _, err = c.Writer.Write(responseBody) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + + if rerankResponse.Meta.Tokens == nil { + return &model.Usage{ + PromptTokens: meta.PromptTokens, + CompletionTokens: 0, + TotalTokens: meta.PromptTokens, + }, nil + } + if rerankResponse.Meta.Tokens.InputTokens <= 0 { + rerankResponse.Meta.Tokens.InputTokens = meta.PromptTokens + } + return &model.Usage{ + PromptTokens: rerankResponse.Meta.Tokens.InputTokens, + CompletionTokens: rerankResponse.Meta.Tokens.OutputTokens, + TotalTokens: rerankResponse.Meta.Tokens.InputTokens + rerankResponse.Meta.Tokens.OutputTokens, + }, nil +} diff --git a/service/aiproxy/relay/adaptor/openai/stt.go b/service/aiproxy/relay/adaptor/openai/stt.go new file mode 100644 index 00000000000..a4f8fec6673 --- /dev/null +++ b/service/aiproxy/relay/adaptor/openai/stt.go @@ -0,0 +1,176 @@ +package openai + +import ( + "bufio" + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common/conv" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/meta" + "github.com/labring/sealos/service/aiproxy/relay/model" +) + +func ConvertSTTRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { + err := request.ParseMultipartForm(1024 * 1024 * 4) + if err != nil { + return nil, nil, err + } + + multipartBody := &bytes.Buffer{} + multipartWriter := multipart.NewWriter(multipartBody) + + for key, values := range request.MultipartForm.Value { + if len(values) == 0 { + continue + } + value := values[0] + if key == "model" { + err = multipartWriter.WriteField(key, meta.ActualModelName) + if err != nil { + return nil, nil, err + } + continue + } + if key == "response_format" { + meta.Set(MetaResponseFormat, value) + continue + } + err = multipartWriter.WriteField(key, value) + if err != nil { + return nil, nil, err + } + } + + for key, files := range request.MultipartForm.File { + if len(files) == 0 { + continue + } + fileHeader := files[0] + file, err := fileHeader.Open() + if err != nil { + return nil, nil, err + } + w, err := multipartWriter.CreateFormFile(key, fileHeader.Filename) + if err != nil { + file.Close() + return nil, nil, err + } + _, err = io.Copy(w, file) + file.Close() + if err != nil { + return nil, nil, err + } + } + + multipartWriter.Close() + ContentType := multipartWriter.FormDataContentType() + return http.Header{ + "Content-Type": {ContentType}, + }, multipartBody, nil +} + +func STTHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *model.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + responseFormat := meta.GetString(MetaResponseFormat) + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + + var openAIErr SlimTextResponse + if err = json.Unmarshal(responseBody, &openAIErr); err == nil { + if openAIErr.Error.Message != "" { + return nil, ErrorWrapper(fmt.Errorf("type %s, code %v, message %s", openAIErr.Error.Type, openAIErr.Error.Code, openAIErr.Error.Message), "request_error", http.StatusInternalServerError) + } + } + + var text string + switch responseFormat { + case "text": + text = getTextFromText(responseBody) + case "srt": + text, err = getTextFromSRT(responseBody) + case "verbose_json": + text, err = getTextFromVerboseJSON(responseBody) + case "vtt": + text, err = getTextFromVTT(responseBody) + case "json": + fallthrough + default: + text, err = getTextFromJSON(responseBody) + } + if err != nil { + return nil, ErrorWrapper(err, "get_text_from_body_err", http.StatusInternalServerError) + } + completionTokens := CountTokenText(text, meta.ActualModelName) + + for k, v := range resp.Header { + c.Writer.Header().Set(k, v[0]) + } + _, err = c.Writer.Write(responseBody) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + + return &model.Usage{ + PromptTokens: 0, + CompletionTokens: completionTokens, + TotalTokens: completionTokens, + }, nil +} + +func getTextFromVTT(body []byte) (string, error) { + return getTextFromSRT(body) +} + +func getTextFromVerboseJSON(body []byte) (string, error) { + var whisperResponse WhisperVerboseJSONResponse + if err := json.Unmarshal(body, &whisperResponse); err != nil { + return "", fmt.Errorf("unmarshal_response_body_failed err :%w", err) + } + return whisperResponse.Text, nil +} + +func getTextFromSRT(body []byte) (string, error) { + scanner := bufio.NewScanner(bytes.NewReader(body)) + var builder strings.Builder + var textLine bool + for scanner.Scan() { + line := scanner.Text() + if textLine { + builder.WriteString(line) + textLine = false + continue + } else if strings.Contains(line, "-->") { + textLine = true + continue + } + } + if err := scanner.Err(); err != nil { + return "", err + } + return builder.String(), nil +} + +func getTextFromText(body []byte) string { + return strings.TrimSuffix(conv.BytesToString(body), "\n") +} + +func getTextFromJSON(body []byte) (string, error) { + var whisperResponse WhisperJSONResponse + if err := json.Unmarshal(body, &whisperResponse); err != nil { + return "", fmt.Errorf("unmarshal_response_body_failed err :%w", err) + } + return whisperResponse.Text, nil +} diff --git a/service/aiproxy/relay/adaptor/openai/token.go b/service/aiproxy/relay/adaptor/openai/token.go index c9607b0e6d0..99b8a98e194 100644 --- a/service/aiproxy/relay/adaptor/openai/token.go +++ b/service/aiproxy/relay/adaptor/openai/token.go @@ -10,9 +10,9 @@ import ( "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/image" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/pkoukk/tiktoken-go" + log "github.com/sirupsen/logrus" ) // tokenEncoderMap won't grow after initialization @@ -25,7 +25,7 @@ var ( func init() { gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo") if err != nil { - logger.FatalLog("failed to get gpt-3.5-turbo token encoder: " + err.Error()) + log.Fatal("failed to get gpt-3.5-turbo token encoder: " + err.Error()) } defaultTokenEncoder = gpt35TokenEncoder } @@ -41,7 +41,7 @@ func getTokenEncoder(model string) *tiktoken.Tiktoken { if ok { tokenEncoder, err := tiktoken.EncodingForModel(model) if err != nil { - logger.SysError(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error())) + log.Error(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error())) tokenEncoder = defaultTokenEncoder } tokenEncoderLock.Lock() @@ -59,7 +59,7 @@ func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int { return len(tokenEncoder.Encode(text, nil, nil)) } -func CountTokenMessages(messages []model.Message, model string) int { +func CountTokenMessages(messages []*model.Message, model string) int { tokenEncoder := getTokenEncoder(model) // Reference: // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb @@ -101,7 +101,7 @@ func CountTokenMessages(messages []model.Message, model string) int { } imageTokens, err := countImageTokens(url, detail, model) if err != nil { - logger.SysError("error counting image tokens: " + err.Error()) + log.Error("error counting image tokens: " + err.Error()) } else { tokenNum += imageTokens } @@ -201,6 +201,12 @@ func CountTokenInput(input any, model string) int { switch v := input.(type) { case string: return CountTokenText(v, model) + case []any: + num := 0 + for _, s := range v { + num += CountTokenInput(s, model) + } + return num case []string: text := "" for _, s := range v { @@ -215,6 +221,9 @@ func CountTokenText(text string, model string) int { if strings.HasPrefix(model, "tts") { return utf8.RuneCountInString(text) } + if strings.HasPrefix(model, "sambert-") { + return len(text) + } tokenEncoder := getTokenEncoder(model) return getTokenNum(tokenEncoder, text) } diff --git a/service/aiproxy/relay/adaptor/openai/tts.go b/service/aiproxy/relay/adaptor/openai/tts.go new file mode 100644 index 00000000000..60fe18094e0 --- /dev/null +++ b/service/aiproxy/relay/adaptor/openai/tts.go @@ -0,0 +1,57 @@ +package openai + +import ( + "bytes" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + textRequest := relaymodel.TextToSpeechRequest{} + err := common.UnmarshalBodyReusable(req, &textRequest) + if err != nil { + return nil, nil, err + } + if len(textRequest.Input) > 4096 { + return nil, nil, errors.New("input is too long (over 4096 characters)") + } + reqMap := make(map[string]any) + err = common.UnmarshalBodyReusable(req, &reqMap) + if err != nil { + return nil, nil, err + } + reqMap["model"] = meta.ActualModelName + jsonData, err := json.Marshal(reqMap) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(jsonData), nil +} + +func TTSHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + defer resp.Body.Close() + + log := middleware.GetLogger(c) + + for k, v := range resp.Header { + c.Writer.Header().Set(k, v[0]) + } + + _, err := io.Copy(c.Writer, resp.Body) + if err != nil { + log.Error("write response body failed: " + err.Error()) + } + return &relaymodel.Usage{ + PromptTokens: meta.PromptTokens, + CompletionTokens: 0, + TotalTokens: meta.PromptTokens, + }, nil +} diff --git a/service/aiproxy/relay/adaptor/openai/util.go b/service/aiproxy/relay/adaptor/openai/util.go index b37dd52571e..1b4c7d8ff23 100644 --- a/service/aiproxy/relay/adaptor/openai/util.go +++ b/service/aiproxy/relay/adaptor/openai/util.go @@ -1,15 +1,42 @@ package openai -import "github.com/labring/sealos/service/aiproxy/relay/model" +import ( + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) -func ErrorWrapper(err error, code string, statusCode int) *model.ErrorWithStatusCode { - Error := model.Error{ - Message: err.Error(), - Type: "aiproxy_error", - Code: code, +func ErrorWrapper(err error, code any, statusCode int) *relaymodel.ErrorWithStatusCode { + return &relaymodel.ErrorWithStatusCode{ + Error: relaymodel.Error{ + Message: err.Error(), + Type: middleware.ErrorTypeAIPROXY, + Code: code, + }, + StatusCode: statusCode, } - return &model.ErrorWithStatusCode{ - Error: Error, +} + +func ErrorWrapperWithMessage(message string, code any, statusCode int) *relaymodel.ErrorWithStatusCode { + return &relaymodel.ErrorWithStatusCode{ + Error: relaymodel.Error{ + Message: message, + Type: middleware.ErrorTypeAIPROXY, + Code: code, + }, StatusCode: statusCode, } } + +func GetPromptTokens(meta *meta.Meta, textRequest *relaymodel.GeneralOpenAIRequest) int { + switch meta.Mode { + case relaymode.ChatCompletions: + return CountTokenMessages(textRequest.Messages, textRequest.Model) + case relaymode.Completions: + return CountTokenInput(textRequest.Prompt, textRequest.Model) + case relaymode.Moderations: + return CountTokenInput(textRequest.Input, textRequest.Model) + } + return 0 +} diff --git a/service/aiproxy/relay/adaptor/palm/adaptor.go b/service/aiproxy/relay/adaptor/palm/adaptor.go deleted file mode 100644 index e65dde874a1..00000000000 --- a/service/aiproxy/relay/adaptor/palm/adaptor.go +++ /dev/null @@ -1,73 +0,0 @@ -package palm - -import ( - "errors" - "io" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -type Adaptor struct{} - -func (a *Adaptor) Init(_ *meta.Meta) { -} - -func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/v1beta2/models/chat-bison-001:generateMessage", nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("X-Goog-Api-Key", meta.APIKey) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return ConvertRequest(request), nil -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - var responseText string - err, responseText = StreamHandler(c, resp) - usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) - } else { - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) - } - return -} - -func (a *Adaptor) GetModelList() []string { - return ModelList -} - -func (a *Adaptor) GetChannelName() string { - return "google palm" -} diff --git a/service/aiproxy/relay/adaptor/palm/constants.go b/service/aiproxy/relay/adaptor/palm/constants.go deleted file mode 100644 index a8349362c25..00000000000 --- a/service/aiproxy/relay/adaptor/palm/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package palm - -var ModelList = []string{ - "PaLM-2", -} diff --git a/service/aiproxy/relay/adaptor/palm/model.go b/service/aiproxy/relay/adaptor/palm/model.go deleted file mode 100644 index 5f46f82f485..00000000000 --- a/service/aiproxy/relay/adaptor/palm/model.go +++ /dev/null @@ -1,40 +0,0 @@ -package palm - -import ( - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -type ChatMessage struct { - Author string `json:"author"` - Content string `json:"content"` -} - -type Filter struct { - Reason string `json:"reason"` - Message string `json:"message"` -} - -type Prompt struct { - Messages []ChatMessage `json:"messages"` -} - -type ChatRequest struct { - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"topP,omitempty"` - Prompt Prompt `json:"prompt"` - CandidateCount int `json:"candidateCount,omitempty"` - TopK int `json:"topK,omitempty"` -} - -type Error struct { - Message string `json:"message"` - Status string `json:"status"` - Code int `json:"code"` -} - -type ChatResponse struct { - Candidates []ChatMessage `json:"candidates"` - Messages []model.Message `json:"messages"` - Filters []Filter `json:"filters"` - Error Error `json:"error"` -} diff --git a/service/aiproxy/relay/adaptor/palm/palm.go b/service/aiproxy/relay/adaptor/palm/palm.go deleted file mode 100644 index 41921a93894..00000000000 --- a/service/aiproxy/relay/adaptor/palm/palm.go +++ /dev/null @@ -1,147 +0,0 @@ -package palm - -import ( - "net/http" - - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/render" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/common/random" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/constant" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#request-body -// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#response-body - -func ConvertRequest(textRequest *model.GeneralOpenAIRequest) *ChatRequest { - palmRequest := ChatRequest{ - Prompt: Prompt{ - Messages: make([]ChatMessage, 0, len(textRequest.Messages)), - }, - Temperature: textRequest.Temperature, - CandidateCount: textRequest.N, - TopP: textRequest.TopP, - TopK: textRequest.MaxTokens, - } - for _, message := range textRequest.Messages { - palmMessage := ChatMessage{ - Content: message.StringContent(), - } - if message.Role == "user" { - palmMessage.Author = "0" - } else { - palmMessage.Author = "1" - } - palmRequest.Prompt.Messages = append(palmRequest.Prompt.Messages, palmMessage) - } - return &palmRequest -} - -func responsePaLM2OpenAI(response *ChatResponse) *openai.TextResponse { - fullTextResponse := openai.TextResponse{ - Choices: make([]openai.TextResponseChoice, 0, len(response.Candidates)), - } - for i, candidate := range response.Candidates { - choice := openai.TextResponseChoice{ - Index: i, - Message: model.Message{ - Role: "assistant", - Content: candidate.Content, - }, - FinishReason: "stop", - } - fullTextResponse.Choices = append(fullTextResponse.Choices, choice) - } - return &fullTextResponse -} - -func streamResponsePaLM2OpenAI(palmResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { - var choice openai.ChatCompletionsStreamResponseChoice - if len(palmResponse.Candidates) > 0 { - choice.Delta.Content = palmResponse.Candidates[0].Content - } - choice.FinishReason = &constant.StopFinishReason - var response openai.ChatCompletionsStreamResponse - response.Object = "chat.completion.chunk" - response.Model = "palm2" - response.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} - return &response -} - -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, string) { - defer resp.Body.Close() - - responseText := "" - responseID := "chatcmpl-" + random.GetUUID() - createdTime := helper.GetTimestamp() - - var palmResponse ChatResponse - err := json.NewDecoder(resp.Body).Decode(&palmResponse) - if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), "" - } - - common.SetEventStreamHeaders(c) - - fullTextResponse := streamResponsePaLM2OpenAI(&palmResponse) - fullTextResponse.ID = responseID - fullTextResponse.Created = createdTime - if len(palmResponse.Candidates) > 0 { - responseText = palmResponse.Candidates[0].Content - } - - err = render.ObjectData(c, fullTextResponse) - if err != nil { - logger.SysError("error stream response: " + err.Error()) - return openai.ErrorWrapper(err, "stream_response_failed", http.StatusInternalServerError), "" - } - - render.Done(c) - - return nil, responseText -} - -func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var palmResponse ChatResponse - err := json.NewDecoder(resp.Body).Decode(&palmResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - if palmResponse.Error.Code != 0 || len(palmResponse.Candidates) == 0 { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: palmResponse.Error.Message, - Type: palmResponse.Error.Status, - Param: "", - Code: palmResponse.Error.Code, - }, - StatusCode: resp.StatusCode, - }, nil - } - fullTextResponse := responsePaLM2OpenAI(&palmResponse) - fullTextResponse.Model = modelName - completionTokens := openai.CountTokenText(palmResponse.Candidates[0].Content, modelName) - usage := model.Usage{ - PromptTokens: promptTokens, - CompletionTokens: completionTokens, - TotalTokens: promptTokens + completionTokens, - } - fullTextResponse.Usage = usage - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, &usage -} diff --git a/service/aiproxy/relay/adaptor/siliconflow/adaptor.go b/service/aiproxy/relay/adaptor/siliconflow/adaptor.go new file mode 100644 index 00000000000..f2816ef5a0f --- /dev/null +++ b/service/aiproxy/relay/adaptor/siliconflow/adaptor.go @@ -0,0 +1,58 @@ +package siliconflow + +import ( + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var _ adaptor.Adaptor = (*Adaptor)(nil) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.siliconflow.cn" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "siliconflow" +} + +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + return a.Adaptor.ConvertRequest(meta, req) +} + +//nolint:gocritic +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (*relaymodel.Usage, *relaymodel.ErrorWithStatusCode) { + usage, err := a.Adaptor.DoResponse(meta, c, resp) + if err != nil { + return nil, err + } + switch meta.Mode { + case relaymode.AudioSpeech: + size := c.Writer.Size() + usage = &relaymodel.Usage{ + CompletionTokens: size, + TotalTokens: size, + } + } + return usage, nil +} diff --git a/service/aiproxy/relay/adaptor/siliconflow/balance.go b/service/aiproxy/relay/adaptor/siliconflow/balance.go new file mode 100644 index 00000000000..74172143d73 --- /dev/null +++ b/service/aiproxy/relay/adaptor/siliconflow/balance.go @@ -0,0 +1,66 @@ +package siliconflow + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/labring/sealos/service/aiproxy/common/client" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" +) + +var _ adaptor.GetBalance = (*Adaptor)(nil) + +func (a *Adaptor) GetBalance(channel *model.Channel) (float64, error) { + u := channel.BaseURL + if u == "" { + u = baseURL + } + url := u + "/v1/user/info" + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return 0, err + } + req.Header.Set("Authorization", "Bearer "+channel.Key) + res, err := client.HTTPClient.Do(req) + if err != nil { + return 0, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return 0, fmt.Errorf("status code: %d", res.StatusCode) + } + response := UsageResponse{} + err = json.NewDecoder(res.Body).Decode(&response) + if err != nil { + return 0, err + } + balance, err := strconv.ParseFloat(response.Data.Balance, 64) + if err != nil { + return 0, err + } + return balance, nil +} + +type UsageResponse struct { + Message string `json:"message"` + Data struct { + ID string `json:"id"` + Name string `json:"name"` + Image string `json:"image"` + Email string `json:"email"` + Balance string `json:"balance"` + Status string `json:"status"` + Introduction string `json:"introduction"` + Role string `json:"role"` + ChargeBalance string `json:"chargeBalance"` + TotalBalance string `json:"totalBalance"` + Category string `json:"category"` + IsAdmin bool `json:"isAdmin"` + } `json:"data"` + Code int `json:"code"` + Status bool `json:"status"` +} diff --git a/service/aiproxy/relay/adaptor/siliconflow/constants.go b/service/aiproxy/relay/adaptor/siliconflow/constants.go index 0bf547611a9..7fbb0b7855c 100644 --- a/service/aiproxy/relay/adaptor/siliconflow/constants.go +++ b/service/aiproxy/relay/adaptor/siliconflow/constants.go @@ -1,36 +1,76 @@ package siliconflow +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://docs.siliconflow.cn/docs/getting-started -var ModelList = []string{ - "deepseek-ai/deepseek-llm-67b-chat", - "Qwen/Qwen1.5-14B-Chat", - "Qwen/Qwen1.5-7B-Chat", - "Qwen/Qwen1.5-110B-Chat", - "Qwen/Qwen1.5-32B-Chat", - "01-ai/Yi-1.5-6B-Chat", - "01-ai/Yi-1.5-9B-Chat-16K", - "01-ai/Yi-1.5-34B-Chat-16K", - "THUDM/chatglm3-6b", - "deepseek-ai/DeepSeek-V2-Chat", - "THUDM/glm-4-9b-chat", - "Qwen/Qwen2-72B-Instruct", - "Qwen/Qwen2-7B-Instruct", - "Qwen/Qwen2-57B-A14B-Instruct", - "deepseek-ai/DeepSeek-Coder-V2-Instruct", - "Qwen/Qwen2-1.5B-Instruct", - "internlm/internlm2_5-7b-chat", - "BAAI/bge-large-en-v1.5", - "BAAI/bge-large-zh-v1.5", - "Pro/Qwen/Qwen2-7B-Instruct", - "Pro/Qwen/Qwen2-1.5B-Instruct", - "Pro/Qwen/Qwen1.5-7B-Chat", - "Pro/THUDM/glm-4-9b-chat", - "Pro/THUDM/chatglm3-6b", - "Pro/01-ai/Yi-1.5-9B-Chat-16K", - "Pro/01-ai/Yi-1.5-6B-Chat", - "Pro/google/gemma-2-9b-it", - "Pro/internlm/internlm2_5-7b-chat", - "Pro/meta-llama/Meta-Llama-3-8B-Instruct", - "Pro/mistralai/Mistral-7B-Instruct-v0.2", +var ModelList = []*model.ModelConfig{ + { + Model: "BAAI/bge-reranker-v2-m3", + Type: relaymode.Rerank, + Owner: model.ModelOwnerBAAI, + InputPrice: 0.0009, + OutputPrice: 0, + }, + + { + Model: "BAAI/bge-large-zh-v1.5", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerBAAI, + }, + + { + Model: "fishaudio/fish-speech-1.4", + Type: relaymode.AudioSpeech, + Owner: model.ModelOwnerFishAudio, + OutputPrice: 0.105, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigSupportVoicesKey: []string{ + "fishaudio/fish-speech-1.4:alex", + "fishaudio/fish-speech-1.4:benjamin", + "fishaudio/fish-speech-1.4:charles", + "fishaudio/fish-speech-1.4:david", + "fishaudio/fish-speech-1.4:anna", + "fishaudio/fish-speech-1.4:bella", + "fishaudio/fish-speech-1.4:claire", + "fishaudio/fish-speech-1.4:diana", + }, + }, + }, + + { + Model: "FunAudioLLM/SenseVoiceSmall", + Type: relaymode.AudioTranscription, + Owner: model.ModelOwnerFunAudioLLM, + }, + + { + Model: "stabilityai/stable-diffusion-3-5-large", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + ImagePrices: map[string]float64{ + "1024x1024": 0, + "512x1024": 0, + "768x512": 0, + "768x1024": 0, + "1024x576": 0, + "576x1024": 0, + }, + }, + { + Model: "stabilityai/stable-diffusion-3-5-large-turbo", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerStabilityAI, + ImagePrices: map[string]float64{ + "1024x1024": 0, + "512x1024": 0, + "768x512": 0, + "768x1024": 0, + "1024x576": 0, + "576x1024": 0, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/siliconflow/image.go b/service/aiproxy/relay/adaptor/siliconflow/image.go new file mode 100644 index 00000000000..08853c733a4 --- /dev/null +++ b/service/aiproxy/relay/adaptor/siliconflow/image.go @@ -0,0 +1,55 @@ +package siliconflow + +import ( + "bytes" + "io" + "net/http" + + json "github.com/json-iterator/go" + + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type ImageRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + NegativePrompt string `json:"negative_prompt"` + ImageSize string `json:"image_size"` + BatchSize int `json:"batch_size"` + Seed int64 `json:"seed"` + NumInferenceSteps int `json:"num_inference_steps"` + GuidanceScale int `json:"guidance_scale"` + PromptEnhancement bool `json:"prompt_enhancement"` +} + +func ConvertImageRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { + var reqMap map[string]any + err := common.UnmarshalBodyReusable(request, &reqMap) + if err != nil { + return nil, nil, err + } + + meta.Set(openai.MetaResponseFormat, reqMap["response_format"]) + + reqMap["model"] = meta.ActualModelName + reqMap["batch_size"] = reqMap["n"] + delete(reqMap, "n") + if _, ok := reqMap["steps"]; ok { + reqMap["num_inference_steps"] = reqMap["steps"] + delete(reqMap, "steps") + } + if _, ok := reqMap["scale"]; ok { + reqMap["guidance_scale"] = reqMap["scale"] + delete(reqMap, "scale") + } + reqMap["image_size"] = reqMap["size"] + delete(reqMap, "size") + + data, err := json.Marshal(&reqMap) + if err != nil { + return nil, nil, err + } + return http.Header{}, bytes.NewReader(data), nil +} diff --git a/service/aiproxy/relay/adaptor/stepfun/adaptor.go b/service/aiproxy/relay/adaptor/stepfun/adaptor.go new file mode 100644 index 00000000000..6359e3d95f5 --- /dev/null +++ b/service/aiproxy/relay/adaptor/stepfun/adaptor.go @@ -0,0 +1,28 @@ +package stepfun + +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/meta" +) + +type Adaptor struct { + openai.Adaptor +} + +const baseURL = "https://api.stepfun.com" + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL + } + return a.Adaptor.GetRequestURL(meta) +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "stepfun" +} diff --git a/service/aiproxy/relay/adaptor/stepfun/constants.go b/service/aiproxy/relay/adaptor/stepfun/constants.go index 6a2346cac5b..944d733a4cb 100644 --- a/service/aiproxy/relay/adaptor/stepfun/constants.go +++ b/service/aiproxy/relay/adaptor/stepfun/constants.go @@ -1,13 +1,54 @@ package stepfun -var ModelList = []string{ - "step-1-8k", - "step-1-32k", - "step-1-128k", - "step-1-256k", - "step-1-flash", - "step-2-16k", - "step-1v-8k", - "step-1v-32k", - "step-1x-medium", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "step-1-8k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1-128k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1-256k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1-flash", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-2-16k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1v-8k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1v-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, + { + Model: "step-1x-medium", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerStepFun, + }, } diff --git a/service/aiproxy/relay/adaptor/tencent/adaptor.go b/service/aiproxy/relay/adaptor/tencent/adaptor.go index f900c483589..0b032d03240 100644 --- a/service/aiproxy/relay/adaptor/tencent/adaptor.go +++ b/service/aiproxy/relay/adaptor/tencent/adaptor.go @@ -1,93 +1,27 @@ package tencent import ( - "errors" - "io" - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" ) // https://cloud.tencent.com/document/api/1729/101837 type Adaptor struct { - meta *meta.Meta - Sign string - Action string - Version string - Timestamp int64 + openai.Adaptor } -func (a *Adaptor) Init(meta *meta.Meta) { - a.Action = "ChatCompletions" - a.Version = "2023-09-01" - a.Timestamp = helper.GetTimestamp() - a.meta = meta -} +const baseURL = "https://api.hunyuan.cloud.tencent.com" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/", nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", a.Sign) - req.Header.Set("X-Tc-Action", a.Action) - req.Header.Set("X-Tc-Version", a.Version) - req.Header.Set("X-Tc-Timestamp", strconv.FormatInt(a.Timestamp, 10)) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - _, secretID, secretKey, err := ParseConfig(a.meta.APIKey) - if err != nil { - return nil, err - } - // we have to calculate the sign here - a.Sign = GetSign(request, a, secretID, secretKey) - return request, nil -} - -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - var responseText string - err, responseText = StreamHandler(c, resp) - usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) - } else { - err, usage = Handler(c, resp) + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL } - return + return a.Adaptor.GetRequestURL(meta) } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/tencent/constants.go b/service/aiproxy/relay/adaptor/tencent/constants.go index e8631e5f476..da5770960d9 100644 --- a/service/aiproxy/relay/adaptor/tencent/constants.go +++ b/service/aiproxy/relay/adaptor/tencent/constants.go @@ -1,9 +1,98 @@ package tencent -var ModelList = []string{ - "hunyuan-lite", - "hunyuan-standard", - "hunyuan-standard-256K", - "hunyuan-pro", - "hunyuan-vision", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "hunyuan-lite", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + }, + { + Model: "hunyuan-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.015, + OutputPrice: 0.05, + }, + { + Model: "hunyuan-pro", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.03, + OutputPrice: 0.10, + }, + { + Model: "hunyuan-large", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.004, + OutputPrice: 0.012, + }, + { + Model: "hunyuan-large-longcontext", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.006, + OutputPrice: 0.018, + }, + { + Model: "hunyuan-standard", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.0008, + OutputPrice: 0.002, + }, + // { + // Model: "hunyuan-standard-256K", + // Type: relaymode.ChatCompletions, + // Owner: model.ModelOwnerTencent, + // InputPrice: 0.0005, + // OutputPrice: 0.002, + // }, + { + Model: "hunyuan-role", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.004, + OutputPrice: 0.008, + }, + { + Model: "hunyuan-functioncall", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.004, + OutputPrice: 0.008, + }, + { + Model: "hunyuan-code", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.004, + OutputPrice: 0.008, + }, + { + Model: "hunyuan-turbo-vision", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.08, + OutputPrice: 0.08, + }, + { + Model: "hunyuan-vision", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerTencent, + InputPrice: 0.018, + OutputPrice: 0.018, + }, + + { + Model: "hunyuan-embedding", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerTencent, + InputPrice: 0.0007, + }, } diff --git a/service/aiproxy/relay/adaptor/tencent/main.go b/service/aiproxy/relay/adaptor/tencent/main.go deleted file mode 100644 index 6d81f5a8f05..00000000000 --- a/service/aiproxy/relay/adaptor/tencent/main.go +++ /dev/null @@ -1,221 +0,0 @@ -package tencent - -import ( - "bufio" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/render" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/common/random" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/constant" - "github.com/labring/sealos/service/aiproxy/relay/model" -) - -func responseTencent2OpenAI(response *ChatResponse) *openai.TextResponse { - fullTextResponse := openai.TextResponse{ - Object: "chat.completion", - Created: helper.GetTimestamp(), - Usage: model.Usage{ - PromptTokens: response.Usage.PromptTokens, - CompletionTokens: response.Usage.CompletionTokens, - TotalTokens: response.Usage.TotalTokens, - }, - } - if len(response.Choices) > 0 { - choice := openai.TextResponseChoice{ - Index: 0, - Message: model.Message{ - Role: "assistant", - Content: response.Choices[0].Messages.Content, - }, - FinishReason: response.Choices[0].FinishReason, - } - fullTextResponse.Choices = append(fullTextResponse.Choices, choice) - } - return &fullTextResponse -} - -func streamResponseTencent2OpenAI(tencentResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { - response := openai.ChatCompletionsStreamResponse{ - ID: "chatcmpl-" + random.GetUUID(), - Object: "chat.completion.chunk", - Created: helper.GetTimestamp(), - Model: "tencent-hunyuan", - } - if len(tencentResponse.Choices) > 0 { - var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = tencentResponse.Choices[0].Delta.Content - if tencentResponse.Choices[0].FinishReason == "stop" { - choice.FinishReason = &constant.StopFinishReason - } - response.Choices = append(response.Choices, choice) - } - return &response -} - -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, string) { - defer resp.Body.Close() - - var responseText string - scanner := bufio.NewScanner(resp.Body) - scanner.Split(bufio.ScanLines) - - common.SetEventStreamHeaders(c) - - for scanner.Scan() { - data := scanner.Bytes() - if len(data) < 6 || conv.BytesToString(data[:6]) != "data: " { - continue - } - data = data[6:] - - if conv.BytesToString(data) == "[DONE]" { - break - } - - var tencentResponse ChatResponse - err := json.Unmarshal(data, &tencentResponse) - if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) - continue - } - - response := streamResponseTencent2OpenAI(&tencentResponse) - if len(response.Choices) != 0 { - responseText += conv.AsString(response.Choices[0].Delta.Content) - } - - err = render.ObjectData(c, response) - if err != nil { - logger.SysError(err.Error()) - } - } - - if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) - } - - render.Done(c) - - return nil, responseText -} - -func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var responseP ChatResponseP - err := json.NewDecoder(resp.Body).Decode(&responseP) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - if responseP.Response.Error.Code != 0 { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: responseP.Response.Error.Message, - Code: responseP.Response.Error.Code, - }, - StatusCode: resp.StatusCode, - }, nil - } - fullTextResponse := responseTencent2OpenAI(&responseP.Response) - fullTextResponse.Model = "hunyuan" - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, err = c.Writer.Write(jsonResponse) - if err != nil { - return openai.ErrorWrapper(err, "write_response_body_failed", http.StatusInternalServerError), nil - } - return nil, &fullTextResponse.Usage -} - -func ParseConfig(config string) (appID int64, secretID string, secretKey string, err error) { - parts := strings.Split(config, "|") - if len(parts) != 3 { - err = errors.New("invalid tencent config") - return - } - appID, err = strconv.ParseInt(parts[0], 10, 64) - secretID = parts[1] - secretKey = parts[2] - return -} - -func sha256hex(s string) string { - b := sha256.Sum256(conv.StringToBytes(s)) - return hex.EncodeToString(b[:]) -} - -func hmacSha256(s, key string) string { - hashed := hmac.New(sha256.New, conv.StringToBytes(key)) - hashed.Write(conv.StringToBytes(s)) - return conv.BytesToString(hashed.Sum(nil)) -} - -func GetSign(req *model.GeneralOpenAIRequest, adaptor *Adaptor, secID, secKey string) string { - // build canonical request string - host := "hunyuan.tencentcloudapi.com" - httpRequestMethod := "POST" - canonicalURI := "/" - canonicalQueryString := "" - canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\nx-tc-action:%s\n", - "application/json", host, strings.ToLower(adaptor.Action)) - signedHeaders := "content-type;host;x-tc-action" - payload, _ := json.Marshal(req) - hashedRequestPayload := sha256hex(conv.BytesToString(payload)) - canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", - httpRequestMethod, - canonicalURI, - canonicalQueryString, - canonicalHeaders, - signedHeaders, - hashedRequestPayload) - // build string to sign - algorithm := "TC3-HMAC-SHA256" - requestTimestamp := strconv.FormatInt(adaptor.Timestamp, 10) - timestamp, _ := strconv.ParseInt(requestTimestamp, 10, 64) - t := time.Unix(timestamp, 0).UTC() - // must be the format 2006-01-02, ref to package time for more info - date := t.Format("2006-01-02") - credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, "hunyuan") - hashedCanonicalRequest := sha256hex(canonicalRequest) - string2sign := fmt.Sprintf("%s\n%s\n%s\n%s", - algorithm, - requestTimestamp, - credentialScope, - hashedCanonicalRequest) - - // sign string - secretDate := hmacSha256(date, "TC3"+secKey) - secretService := hmacSha256("hunyuan", secretDate) - secretKey := hmacSha256("tc3_request", secretService) - signature := hex.EncodeToString(conv.StringToBytes(hmacSha256(string2sign, secretKey))) - - // build authorization - authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", - algorithm, - secID, - credentialScope, - signedHeaders, - signature) - return authorization -} diff --git a/service/aiproxy/relay/adaptor/tencent/model.go b/service/aiproxy/relay/adaptor/tencent/model.go deleted file mode 100644 index 1e3f1ae61b1..00000000000 --- a/service/aiproxy/relay/adaptor/tencent/model.go +++ /dev/null @@ -1,34 +0,0 @@ -package tencent - -import "github.com/labring/sealos/service/aiproxy/relay/model" - -type Error struct { - Message string `json:"Message"` - Code int `json:"Code"` -} - -type Usage struct { - PromptTokens int `json:"PromptTokens"` - CompletionTokens int `json:"CompletionTokens"` - TotalTokens int `json:"TotalTokens"` -} - -type ResponseChoices struct { - FinishReason string `json:"FinishReason,omitempty"` // 流式结束标志位,为 stop 则表示尾包 - Messages model.Message `json:"Message,omitempty"` // 内容,同步模式返回内容,流模式为 null 输出 content 内容总数最多支持 1024token。 - Delta model.Message `json:"Delta,omitempty"` // 内容,流模式返回内容,同步模式为 null 输出 content 内容总数最多支持 1024token。 -} - -type ChatResponse struct { - ID string `json:"Id,omitempty"` - Note string `json:"Note,omitempty"` - ReqID string `json:"Req_id,omitempty"` - Choices []ResponseChoices `json:"Choices,omitempty"` - Error Error `json:"Error,omitempty"` - Usage Usage `json:"Usage,omitempty"` - Created int64 `json:"Created,omitempty"` -} - -type ChatResponseP struct { - Response ChatResponse `json:"Response,omitempty"` -} diff --git a/service/aiproxy/relay/adaptor/togetherai/constants.go b/service/aiproxy/relay/adaptor/togetherai/constants.go index 0a79fbdcc5a..dd3db44f496 100644 --- a/service/aiproxy/relay/adaptor/togetherai/constants.go +++ b/service/aiproxy/relay/adaptor/togetherai/constants.go @@ -1,10 +1,40 @@ package togetherai +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + // https://docs.together.ai/docs/inference-models -var ModelList = []string{ - "meta-llama/Llama-3-70b-chat-hf", - "deepseek-ai/deepseek-coder-33b-instruct", - "mistralai/Mixtral-8x22B-Instruct-v0.1", - "Qwen/Qwen1.5-72B-Chat", +var ModelList = []*model.ModelConfig{ + { + Model: "meta-llama/Llama-3-70b-chat-hf", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMeta, + }, + { + Model: "deepseek-ai/deepseek-coder-33b-instruct", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerDeepSeek, + }, + { + Model: "mistralai/Mixtral-8x22B-Instruct-v0.1", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerMistral, + }, + { + Model: "Qwen/Qwen1.5-72B-Chat", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAlibaba, + }, +} + +type Adaptor struct { + openai.Adaptor +} + +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return ModelList } diff --git a/service/aiproxy/relay/adaptor/vertexai/adaptor.go b/service/aiproxy/relay/adaptor/vertexai/adaptor.go index 829cfc6465d..fa8ab21ce1f 100644 --- a/service/aiproxy/relay/adaptor/vertexai/adaptor.go +++ b/service/aiproxy/relay/adaptor/vertexai/adaptor.go @@ -1,6 +1,7 @@ package vertexai import ( + "context" "errors" "fmt" "io" @@ -8,9 +9,12 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" channelhelper "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) var _ channelhelper.Adaptor = new(Adaptor) @@ -19,46 +23,25 @@ const channelName = "vertexai" type Adaptor struct{} -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*relaymodel.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) Init(_ *meta.Meta) { -} - -func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *relaymodel.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - - adaptor := GetAdaptor(request.Model) +func (a *Adaptor) ConvertRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { + adaptor := GetAdaptor(meta.ActualModelName) if adaptor == nil { - return nil, errors.New("adaptor not found") + return nil, nil, errors.New("adaptor not found") } - return adaptor.ConvertRequest(c, relayMode, request) + return adaptor.ConvertRequest(meta, request) } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { adaptor := GetAdaptor(meta.ActualModelName) if adaptor == nil { - return nil, &relaymodel.ErrorWithStatusCode{ - StatusCode: http.StatusInternalServerError, - Error: relaymodel.Error{ - Message: "adaptor not found", - }, - } + return nil, openai.ErrorWrapperWithMessage(meta.ActualModelName+" adaptor not found", "adaptor_not_found", http.StatusInternalServerError) } - return adaptor.DoResponse(c, resp, meta) + return adaptor.DoResponse(meta, c, resp) } -func (a *Adaptor) GetModelList() (models []string) { - models = modelList - return +func (a *Adaptor) GetModelList() []*model.ModelConfig { + return modelList } func (a *Adaptor) GetChannelName() string { @@ -68,42 +51,41 @@ func (a *Adaptor) GetChannelName() string { func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { var suffix string if strings.HasPrefix(meta.ActualModelName, "gemini") { - if meta.IsStream { + if meta.GetBool("stream") { suffix = "streamGenerateContent?alt=sse" } else { suffix = "generateContent" } } else { - if meta.IsStream { + if meta.GetBool("stream") { suffix = "streamRawPredict?alt=sse" } else { suffix = "rawPredict" } } - if meta.BaseURL != "" { + if meta.Channel.BaseURL != "" { return fmt.Sprintf( "%s/v1/projects/%s/locations/%s/publishers/google/models/%s:%s", - meta.BaseURL, - meta.Config.VertexAIProjectID, - meta.Config.Region, + meta.Channel.BaseURL, + meta.Channel.Config.VertexAIProjectID, + meta.Channel.Config.Region, meta.ActualModelName, suffix, ), nil } return fmt.Sprintf( "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s", - meta.Config.Region, - meta.Config.VertexAIProjectID, - meta.Config.Region, + meta.Channel.Config.Region, + meta.Channel.Config.VertexAIProjectID, + meta.Channel.Config.Region, meta.ActualModelName, suffix, ), nil } -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - channelhelper.SetupCommonRequestHeader(c, req, meta) - token, err := getToken(c, meta.ChannelID, meta.Config.VertexAIADC) +func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error { + token, err := getToken(context.Background(), meta.Channel.ID, meta.Channel.Config.VertexAIADC) if err != nil { return err } @@ -111,13 +93,6 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *me return nil } -func (a *Adaptor) ConvertImageRequest(request *relaymodel.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return channelhelper.DoRequestHelper(a, c, meta, requestBody) +func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, req *http.Request) (*http.Response, error) { + return utils.DoRequest(req) } diff --git a/service/aiproxy/relay/adaptor/vertexai/claude/adapter.go b/service/aiproxy/relay/adaptor/vertexai/claude/adapter.go index bb55f4dbf24..8337d7e6ee8 100644 --- a/service/aiproxy/relay/adaptor/vertexai/claude/adapter.go +++ b/service/aiproxy/relay/adaptor/vertexai/claude/adapter.go @@ -1,36 +1,70 @@ package vertexai import ( + "bytes" + "io" "net/http" + json "github.com/json-iterator/go" + "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/anthropic" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" "github.com/pkg/errors" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) -var ModelList = []string{ - "claude-3-haiku@20240307", - "claude-3-sonnet@20240229", - "claude-3-opus@20240229", - "claude-3-5-sonnet@20240620", - "claude-3-5-sonnet-v2@20241022", - "claude-3-5-haiku@20241022", +var ModelList = []*model.ModelConfig{ + { + Model: "claude-3-haiku@20240307", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-sonnet@20240229", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-opus@20240229", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-5-sonnet@20240620", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-5-sonnet-v2@20241022", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, + { + Model: "claude-3-5-haiku@20241022", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerAnthropic, + }, } const anthropicVersion = "vertex-2023-10-16" type Adaptor struct{} -func (a *Adaptor) ConvertRequest(c *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { if request == nil { - return nil, errors.New("request is nil") + return nil, nil, errors.New("request is nil") } - claudeReq := anthropic.ConvertRequest(request) + claudeReq, err := anthropic.ConvertRequest(meta, request) + if err != nil { + return nil, nil, err + } + meta.Set("stream", claudeReq.Stream) req := Request{ AnthropicVersion: anthropicVersion, // Model: claudeReq.Model, @@ -43,17 +77,18 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, _ int, request *model.GeneralOp Stream: claudeReq.Stream, Tools: claudeReq.Tools, } - - c.Set(ctxkey.RequestModel, request.Model) - c.Set(ctxkey.ConvertedRequest, req) - return req, nil + data, err := json.Marshal(req) + if err != nil { + return nil, nil, err + } + return nil, bytes.NewReader(data), nil } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = anthropic.StreamHandler(c, resp) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + if utils.IsStreamResponse(resp) { + err, usage = anthropic.StreamHandler(meta, c, resp) } else { - err, usage = anthropic.Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + err, usage = anthropic.Handler(meta, c, resp) } return } diff --git a/service/aiproxy/relay/adaptor/vertexai/gemini/adapter.go b/service/aiproxy/relay/adaptor/vertexai/gemini/adapter.go index 861accadf5f..8d50a3d4f94 100644 --- a/service/aiproxy/relay/adaptor/vertexai/gemini/adapter.go +++ b/service/aiproxy/relay/adaptor/vertexai/gemini/adapter.go @@ -1,47 +1,57 @@ package vertexai import ( + "io" "net/http" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/gemini" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/relaymode" - "github.com/pkg/errors" + "github.com/labring/sealos/service/aiproxy/relay/utils" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) -var ModelList = []string{ - "gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision", +var ModelList = []*model.ModelConfig{ + { + Model: "gemini-1.5-pro-001", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemini-1.5-flash-001", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemini-pro", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, + { + Model: "gemini-pro-vision", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerGoogle, + }, } type Adaptor struct{} -func (a *Adaptor) ConvertRequest(c *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - - geminiRequest := gemini.ConvertRequest(request) - c.Set(ctxkey.RequestModel, request.Model) - c.Set(ctxkey.ConvertedRequest, geminiRequest) - return geminiRequest, nil +func (a *Adaptor) ConvertRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { + return gemini.ConvertRequest(meta, request) } -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - var responseText string - err, responseText = gemini.StreamHandler(c, resp) - usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) - } else { - switch meta.Mode { - case relaymode.Embeddings: - err, usage = gemini.EmbeddingHandler(c, resp) - default: - err, usage = gemini.Handler(c, resp, meta.PromptTokens, meta.ActualModelName) +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { + switch meta.Mode { + case relaymode.Embeddings: + usage, err = gemini.EmbeddingHandler(c, resp) + default: + if utils.IsStreamResponse(resp) { + usage, err = gemini.StreamHandler(meta, c, resp) + } else { + usage, err = gemini.Handler(meta, c, resp) } } return diff --git a/service/aiproxy/relay/adaptor/vertexai/registry.go b/service/aiproxy/relay/adaptor/vertexai/registry.go index ee95a19a91d..4173bf8f803 100644 --- a/service/aiproxy/relay/adaptor/vertexai/registry.go +++ b/service/aiproxy/relay/adaptor/vertexai/registry.go @@ -1,13 +1,15 @@ package vertexai import ( + "io" "net/http" "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/model" claude "github.com/labring/sealos/service/aiproxy/relay/adaptor/vertexai/claude" gemini "github.com/labring/sealos/service/aiproxy/relay/adaptor/vertexai/gemini" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" ) type ModelType int @@ -19,24 +21,24 @@ const ( var ( modelMapping = map[string]ModelType{} - modelList = []string{} + modelList = []*model.ModelConfig{} ) func init() { - modelList = append(modelList, claude.ModelList...) for _, model := range claude.ModelList { - modelMapping[model] = VerterAIClaude + modelMapping[model.Model] = VerterAIClaude + modelList = append(modelList, model) } - modelList = append(modelList, gemini.ModelList...) for _, model := range gemini.ModelList { - modelMapping[model] = VerterAIGemini + modelMapping[model.Model] = VerterAIGemini + modelList = append(modelList, model) } } type innerAIAdapter interface { - ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) - DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) + ConvertRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) + DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) } func GetAdaptor(model string) innerAIAdapter { diff --git a/service/aiproxy/relay/adaptor/xunfei/adaptor.go b/service/aiproxy/relay/adaptor/xunfei/adaptor.go index 0e6b917e8b6..46d966df711 100644 --- a/service/aiproxy/relay/adaptor/xunfei/adaptor.go +++ b/service/aiproxy/relay/adaptor/xunfei/adaptor.go @@ -4,70 +4,42 @@ import ( "io" "net/http" - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" ) type Adaptor struct { - meta *meta.Meta + openai.Adaptor } -func (a *Adaptor) Init(meta *meta.Meta) { - a.meta = meta -} +const baseURL = "https://spark-api-open.xf-yun.com" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - return meta.BaseURL + "/v1/chat/completions", nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - req.Header.Set("Authorization", "Bearer "+meta.APIKey) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, _ int, request *model.GeneralOpenAIRequest) (any, error) { - domain, err := getXunfeiDomain(request.Model) - if err != nil { - return nil, err + if meta.Channel.BaseURL == "" { + meta.Channel.BaseURL = baseURL } - request.Model = domain - return request, nil + return a.Adaptor.GetRequestURL(meta) } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - domain, err := getXunfeiDomain(request.Model) +func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (http.Header, io.Reader, error) { + domain, err := getXunfeiDomain(meta.ActualModelName) if err != nil { - return nil, err + return nil, nil, err } - request.Model = domain - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, usage = StreamHandler(c, resp, meta.PromptTokens, meta.ActualModelName) - } else { - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + model := meta.ActualModelName + meta.ActualModelName = domain + defer func() { + meta.ActualModelName = model + }() + h, body, err := a.Adaptor.ConvertRequest(meta, req) + if err != nil { + return nil, nil, err } - return + return h, body, nil } -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/xunfei/constants.go b/service/aiproxy/relay/adaptor/xunfei/constants.go index f39f5515260..3f6ad14ccec 100644 --- a/service/aiproxy/relay/adaptor/xunfei/constants.go +++ b/service/aiproxy/relay/adaptor/xunfei/constants.go @@ -1,10 +1,69 @@ package xunfei -var ModelList = []string{ - "SparkDesk-Lite", - "SparkDesk-Pro", - "SparkDesk-Pro-128K", - "SparkDesk-Max", - "SparkDesk-Max-32k", - "SparkDesk-4.0-Ultra", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "SparkDesk-4.0-Ultra", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerXunfei, + InputPrice: 0.14, + OutputPrice: 0.14, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + }, + }, + { + Model: "SparkDesk-Lite", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerXunfei, + InputPrice: 0.001, + OutputPrice: 0.001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4000, + }, + }, + { + Model: "SparkDesk-Max", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerXunfei, + InputPrice: 0.06, + OutputPrice: 0.06, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + }, + }, + { + Model: "SparkDesk-Max-32k", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerXunfei, + InputPrice: 0.09, + OutputPrice: 0.09, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 32000, + }, + }, + { + Model: "SparkDesk-Pro", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerXunfei, + InputPrice: 0.014, + OutputPrice: 0.014, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + }, + }, + { + Model: "SparkDesk-Pro-128K", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerXunfei, + InputPrice: 0.026, + OutputPrice: 0.026, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/xunfei/main.go b/service/aiproxy/relay/adaptor/xunfei/main.go index e14f4342197..1d6004dc57b 100644 --- a/service/aiproxy/relay/adaptor/xunfei/main.go +++ b/service/aiproxy/relay/adaptor/xunfei/main.go @@ -1,113 +1,13 @@ package xunfei import ( - "bufio" "errors" - "net/http" "strings" - - json "github.com/json-iterator/go" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/common/render" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/model" ) // https://console.xfyun.cn/services/cbm // https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html -func StreamHandler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - scanner := bufio.NewScanner(resp.Body) - scanner.Split(bufio.ScanLines) - - common.SetEventStreamHeaders(c) - id := helper.GetResponseID(c) - responseModel := c.GetString(ctxkey.OriginalModel) - var responseText string - - var usage *model.Usage - - for scanner.Scan() { - data := scanner.Bytes() - if len(data) < 6 || conv.BytesToString(data[:6]) != "data: " { - continue - } - data = data[6:] - - if conv.BytesToString(data) == "[DONE]" { - break - } - - var response openai.ChatCompletionsStreamResponse - err := json.Unmarshal(data, &response) - if err != nil { - logger.SysErrorf("error unmarshalling stream response: %s, data: %s", err.Error(), conv.BytesToString(data)) - continue - } - - if response.Usage != nil { - usage = response.Usage - } - - for _, v := range response.Choices { - v.Delta.Role = "assistant" - responseText += v.Delta.StringContent() - } - response.ID = id - response.Model = modelName - err = render.ObjectData(c, response) - if err != nil { - logger.SysError(err.Error()) - } - } - - if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) - } - - render.Done(c) - - if usage == nil { - usage = openai.ResponseText2Usage(responseText, responseModel, promptTokens) - } - return nil, usage -} - -func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var response openai.TextResponse - err := json.NewDecoder(resp.Body).Decode(&response) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - response.Model = modelName - var responseText string - for _, v := range response.Choices { - responseText += v.Message.Content.(string) - } - usage := openai.ResponseText2Usage(responseText, modelName, promptTokens) - response.Usage = *usage - response.ID = helper.GetResponseID(c) - jsonResponse, err := json.Marshal(response) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, usage -} - func getXunfeiDomain(modelName string) (string, error) { _, s, ok := strings.Cut(modelName, "-") if !ok { diff --git a/service/aiproxy/relay/adaptor/zhipu/adaptor.go b/service/aiproxy/relay/adaptor/zhipu/adaptor.go index 968d40ff3b7..719e1791afe 100644 --- a/service/aiproxy/relay/adaptor/zhipu/adaptor.go +++ b/service/aiproxy/relay/adaptor/zhipu/adaptor.go @@ -2,153 +2,50 @@ package zhipu import ( "errors" - "fmt" - "io" - "math" "net/http" - "strings" "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/model" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/meta" - "github.com/labring/sealos/service/aiproxy/relay/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) type Adaptor struct { - APIVersion string + openai.Adaptor } -func (a *Adaptor) Init(_ *meta.Meta) { -} - -func (a *Adaptor) SetVersionByModeName(modelName string) { - if strings.HasPrefix(modelName, "glm-") { - a.APIVersion = "v4" - } else { - a.APIVersion = "v3" - } -} +const baseURL = "https://open.bigmodel.cn" func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + u := meta.Channel.BaseURL + if u == "" { + u = baseURL + } switch meta.Mode { case relaymode.ImagesGenerations: - return meta.BaseURL + "/api/paas/v4/images/generations", nil - case relaymode.Embeddings: - return meta.BaseURL + "/api/paas/v4/embeddings", nil - } - a.SetVersionByModeName(meta.ActualModelName) - if a.APIVersion == "v4" { - return meta.BaseURL + "/api/paas/v4/chat/completions", nil - } - method := "invoke" - if meta.IsStream { - method = "sse-invoke" - } - return fmt.Sprintf("%s/api/paas/v3/model-api/%s/%s", meta.BaseURL, meta.ActualModelName, method), nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { - adaptor.SetupCommonRequestHeader(c, req, meta) - token := GetToken(meta.APIKey) - req.Header.Set("Authorization", token) - return nil -} - -func (a *Adaptor) ConvertRequest(_ *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - switch relayMode { + return u + "/api/paas/v4/images/generations", nil case relaymode.Embeddings: - baiduEmbeddingRequest, err := ConvertEmbeddingRequest(*request) - return baiduEmbeddingRequest, err + return u + "/api/paas/v4/embeddings", nil + case relaymode.ChatCompletions: + return u + "/api/paas/v4/chat/completions", nil default: - // TopP (0.0, 1.0) - if request.TopP != nil { - *request.TopP = math.Min(0.99, *request.TopP) - *request.TopP = math.Max(0.01, *request.TopP) - } - - // Temperature (0.0, 1.0) - if request.Temperature != nil { - *request.Temperature = math.Min(0.99, *request.Temperature) - *request.Temperature = math.Max(0.01, *request.Temperature) - } - a.SetVersionByModeName(request.Model) - if a.APIVersion == "v4" { - return request, nil - } - return ConvertRequest(request), nil + return "", errors.New("unsupported mode") } } -func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { - if request == nil { - return nil, errors.New("request is nil") - } - newRequest := ImageRequest{ - Model: request.Model, - Prompt: request.Prompt, - UserID: request.User, - } - return newRequest, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { - return adaptor.DoRequestHelper(a, c, meta, requestBody) -} - -func (a *Adaptor) ConvertSTTRequest(*http.Request) (io.ReadCloser, error) { - return nil, nil -} - -func (a *Adaptor) ConvertTTSRequest(*model.TextToSpeechRequest) (any, error) { - return nil, nil -} - -func (a *Adaptor) DoResponseV4(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { - if meta.IsStream { - err, _, usage = openai.StreamHandler(c, resp, meta.Mode) - } else { - err, usage = openai.Handler(c, resp, meta.PromptTokens, meta.ActualModelName) - } - return -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { +func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *relaymodel.Usage, err *relaymodel.ErrorWithStatusCode) { switch meta.Mode { case relaymode.Embeddings: err, usage = EmbeddingsHandler(c, resp) - return - case relaymode.ImagesGenerations: - err, usage = openai.ImageHandler(c, resp) - return - } - if a.APIVersion == "v4" { - return a.DoResponseV4(c, resp, meta) - } - if meta.IsStream { - err, usage = StreamHandler(c, resp) - } else { - if meta.Mode == relaymode.Embeddings { - err, usage = EmbeddingsHandler(c, resp) - } else { - err, usage = Handler(c, resp) - } + default: + usage, err = openai.DoResponse(meta, c, resp) } return } -func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) (*EmbeddingRequest, error) { - return &EmbeddingRequest{ - Model: request.Model, - Input: request.Input, - }, nil -} - -func (a *Adaptor) GetModelList() []string { +func (a *Adaptor) GetModelList() []*model.ModelConfig { return ModelList } diff --git a/service/aiproxy/relay/adaptor/zhipu/constants.go b/service/aiproxy/relay/adaptor/zhipu/constants.go index e11921230cd..bd001273f38 100644 --- a/service/aiproxy/relay/adaptor/zhipu/constants.go +++ b/service/aiproxy/relay/adaptor/zhipu/constants.go @@ -1,7 +1,214 @@ package zhipu -var ModelList = []string{ - "chatglm_turbo", "chatglm_pro", "chatglm_std", "chatglm_lite", - "glm-4", "glm-4v", "glm-3-turbo", "embedding-2", - "cogview-3", +import ( + "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +var ModelList = []*model.ModelConfig{ + { + Model: "glm-3-turbo", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.001, + OutputPrice: 0.001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.1, + OutputPrice: 0.1, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.05, + OutputPrice: 0.05, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4-air", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.001, + OutputPrice: 0.001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4-airx", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.01, + OutputPrice: 0.01, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8192, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4-long", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.001, + OutputPrice: 0.001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 1000000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4-flashx", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4-flash", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "glm-4v-flash", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 8192, + model.ModelConfigMaxOutputTokensKey: 1024, + }, + }, + { + Model: "glm-4v", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.05, + OutputPrice: 0.05, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 2048, + model.ModelConfigMaxOutputTokensKey: 1024, + }, + }, + { + Model: "glm-4v-plus", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.01, + OutputPrice: 0.01, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 8192, + model.ModelConfigMaxOutputTokensKey: 1024, + }, + }, + + { + Model: "charglm-3", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.015, + OutputPrice: 0.015, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 4096, + model.ModelConfigMaxOutputTokensKey: 2048, + }, + }, + { + Model: "emohaa", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.015, + OutputPrice: 0.015, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 8192, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + { + Model: "codegeex-4", + Type: relaymode.ChatCompletions, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.0001, + OutputPrice: 0.0001, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxContextTokensKey: 128000, + model.ModelConfigMaxOutputTokensKey: 4096, + }, + }, + + { + Model: "embedding-2", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.0005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 8192, + }, + }, + { + Model: "embedding-3", + Type: relaymode.Embeddings, + Owner: model.ModelOwnerChatGLM, + InputPrice: 0.0005, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxInputTokensKey: 8192, + }, + }, + + { + Model: "cogview-3", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerChatGLM, + ImageMaxBatchSize: 1, + ImagePrices: map[string]float64{ + "1024x1024": 0.1, + }, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxOutputTokensKey: 1024, + }, + }, + { + Model: "cogview-3-plus", + Type: relaymode.ImagesGenerations, + Owner: model.ModelOwnerChatGLM, + ImageMaxBatchSize: 1, + ImagePrices: map[string]float64{ + "1024x1024": 0.06, + "768x1344": 0.06, + "864x1152": 0.06, + "1344x768": 0.06, + "1152x864": 0.06, + "1440x720": 0.06, + "720x1440": 0.06, + }, + Config: map[model.ModelConfigKey]any{ + model.ModelConfigMaxOutputTokensKey: 1024, + }, + }, } diff --git a/service/aiproxy/relay/adaptor/zhipu/main.go b/service/aiproxy/relay/adaptor/zhipu/main.go index 5924e3a9acd..099804e5db5 100644 --- a/service/aiproxy/relay/adaptor/zhipu/main.go +++ b/service/aiproxy/relay/adaptor/zhipu/main.go @@ -1,24 +1,12 @@ package zhipu import ( - "bufio" "net/http" - "slices" - "strings" - "sync" - "time" json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/render" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/helper" - "github.com/labring/sealos/service/aiproxy/common/logger" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/constant" "github.com/labring/sealos/service/aiproxy/relay/model" ) @@ -27,213 +15,6 @@ import ( // https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/invoke // https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke -var ( - zhipuTokens sync.Map - expSeconds int64 = 24 * 3600 -) - -func GetToken(apikey string) string { - data, ok := zhipuTokens.Load(apikey) - if ok { - td := data.(tokenData) - if time.Now().Before(td.ExpiryTime) { - return td.Token - } - } - - split := strings.Split(apikey, ".") - if len(split) != 2 { - logger.SysError("invalid zhipu key: " + apikey) - return "" - } - - id := split[0] - secret := split[1] - - expMillis := time.Now().Add(time.Duration(expSeconds)*time.Second).UnixNano() / 1e6 - expiryTime := time.Now().Add(time.Duration(expSeconds) * time.Second) - - timestamp := time.Now().UnixNano() / 1e6 - - payload := jwt.MapClaims{ - "api_key": id, - "exp": expMillis, - "timestamp": timestamp, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) - - token.Header["alg"] = "HS256" - token.Header["sign_type"] = "SIGN" - - tokenString, err := token.SignedString(conv.StringToBytes(secret)) - if err != nil { - return "" - } - - zhipuTokens.Store(apikey, tokenData{ - Token: tokenString, - ExpiryTime: expiryTime, - }) - - return tokenString -} - -func ConvertRequest(request *model.GeneralOpenAIRequest) *Request { - return &Request{ - Prompt: request.Messages, - Temperature: request.Temperature, - TopP: request.TopP, - Incremental: false, - } -} - -func responseZhipu2OpenAI(response *Response) *openai.TextResponse { - fullTextResponse := openai.TextResponse{ - ID: response.Data.TaskID, - Object: "chat.completion", - Created: helper.GetTimestamp(), - Choices: make([]openai.TextResponseChoice, 0, len(response.Data.Choices)), - Usage: response.Data.Usage, - } - for i, choice := range response.Data.Choices { - openaiChoice := openai.TextResponseChoice{ - Index: i, - Message: model.Message{ - Role: choice.Role, - Content: strings.Trim(choice.Content.(string), "\""), - }, - FinishReason: "", - } - if i == len(response.Data.Choices)-1 { - openaiChoice.FinishReason = "stop" - } - fullTextResponse.Choices = append(fullTextResponse.Choices, openaiChoice) - } - return &fullTextResponse -} - -func streamResponseZhipu2OpenAI(zhipuResponse string) *openai.ChatCompletionsStreamResponse { - var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = zhipuResponse - response := openai.ChatCompletionsStreamResponse{ - Object: "chat.completion.chunk", - Created: helper.GetTimestamp(), - Model: "chatglm", - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, - } - return &response -} - -func streamMetaResponseZhipu2OpenAI(zhipuResponse *StreamMetaResponse) (*openai.ChatCompletionsStreamResponse, *model.Usage) { - var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = "" - choice.FinishReason = &constant.StopFinishReason - response := openai.ChatCompletionsStreamResponse{ - ID: zhipuResponse.RequestID, - Object: "chat.completion.chunk", - Created: helper.GetTimestamp(), - Model: "chatglm", - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, - } - return &response, &zhipuResponse.Usage -} - -func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var usage *model.Usage - scanner := bufio.NewScanner(resp.Body) - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - if i := strings.Index(conv.BytesToString(data), "\n\n"); i >= 0 && slices.Contains(data, ':') { - return i + 2, data[0:i], nil - } - if atEOF { - return len(data), data, nil - } - return 0, nil, nil - }) - - common.SetEventStreamHeaders(c) - - for scanner.Scan() { - data := scanner.Text() - lines := strings.Split(data, "\n") - for i, line := range lines { - if len(line) < 6 { - continue - } - if strings.HasPrefix(line, "data: ") { - dataSegment := line[6:] - if i != len(lines)-1 { - dataSegment += "\n" - } - response := streamResponseZhipu2OpenAI(dataSegment) - err := render.ObjectData(c, response) - if err != nil { - logger.SysError("error marshalling stream response: " + err.Error()) - } - } else if strings.HasPrefix(line, "meta: ") { - metaSegment := line[6:] - var zhipuResponse StreamMetaResponse - err := json.Unmarshal(conv.StringToBytes(metaSegment), &zhipuResponse) - if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) - continue - } - response, zhipuUsage := streamMetaResponseZhipu2OpenAI(&zhipuResponse) - err = render.ObjectData(c, response) - if err != nil { - logger.SysError("error marshalling stream response: " + err.Error()) - } - usage = zhipuUsage - } - } - } - - if err := scanner.Err(); err != nil { - logger.SysError("error reading stream: " + err.Error()) - } - - render.Done(c) - - return nil, usage -} - -func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { - defer resp.Body.Close() - - var zhipuResponse Response - err := json.NewDecoder(resp.Body).Decode(&zhipuResponse) - if err != nil { - return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - if !zhipuResponse.Success { - return &model.ErrorWithStatusCode{ - Error: model.Error{ - Message: zhipuResponse.Msg, - Type: "zhipu_error", - Param: "", - Code: zhipuResponse.Code, - }, - StatusCode: resp.StatusCode, - }, nil - } - fullTextResponse := responseZhipu2OpenAI(&zhipuResponse) - fullTextResponse.Model = "chatglm" - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, _ = c.Writer.Write(jsonResponse) - return nil, &fullTextResponse.Usage -} - func EmbeddingsHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { defer resp.Body.Close() @@ -256,7 +37,7 @@ func EmbeddingsHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithSta func embeddingResponseZhipu2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { openAIEmbeddingResponse := openai.EmbeddingResponse{ Object: "list", - Data: make([]openai.EmbeddingResponseItem, 0, len(response.Embeddings)), + Data: make([]*openai.EmbeddingResponseItem, 0, len(response.Embeddings)), Model: response.Model, Usage: model.Usage{ PromptTokens: response.PromptTokens, @@ -266,7 +47,7 @@ func embeddingResponseZhipu2OpenAI(response *EmbeddingResponse) *openai.Embeddin } for _, item := range response.Embeddings { - openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, &openai.EmbeddingResponseItem{ Object: `embedding`, Index: item.Index, Embedding: item.Embedding, diff --git a/service/aiproxy/relay/adaptor/zhipu/model.go b/service/aiproxy/relay/adaptor/zhipu/model.go index e773812cc5b..42148fa18a4 100644 --- a/service/aiproxy/relay/adaptor/zhipu/model.go +++ b/service/aiproxy/relay/adaptor/zhipu/model.go @@ -1,44 +1,15 @@ package zhipu import ( - "time" - "github.com/labring/sealos/service/aiproxy/relay/model" ) type Request struct { - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"top_p,omitempty"` - RequestID string `json:"request_id,omitempty"` - Prompt []model.Message `json:"prompt"` - Incremental bool `json:"incremental,omitempty"` -} - -type ResponseData struct { - TaskID string `json:"task_id"` - RequestID string `json:"request_id"` - TaskStatus string `json:"task_status"` - Choices []model.Message `json:"choices"` - model.Usage `json:"usage"` -} - -type Response struct { - Msg string `json:"msg"` - Data ResponseData `json:"data"` - Code int `json:"code"` - Success bool `json:"success"` -} - -type StreamMetaResponse struct { - RequestID string `json:"request_id"` - TaskID string `json:"task_id"` - TaskStatus string `json:"task_status"` - model.Usage `json:"usage"` -} - -type tokenData struct { - ExpiryTime time.Time - Token string + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"top_p,omitempty"` + RequestID string `json:"request_id,omitempty"` + Prompt []*model.Message `json:"prompt"` + Incremental bool `json:"incremental,omitempty"` } type EmbeddingRequest struct { diff --git a/service/aiproxy/relay/adaptor_test.go b/service/aiproxy/relay/adaptor_test.go deleted file mode 100644 index 14c7eb92cdc..00000000000 --- a/service/aiproxy/relay/adaptor_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package relay - -import ( - "testing" - - "github.com/labring/sealos/service/aiproxy/relay/apitype" - "github.com/smartystreets/goconvey/convey" -) - -func TestGetAdaptor(t *testing.T) { - convey.Convey("get adaptor", t, func() { - for i := 0; i < apitype.Dummy; i++ { - a := GetAdaptor(i) - convey.So(a, convey.ShouldNotBeNil) - } - }) -} diff --git a/service/aiproxy/relay/apitype/define.go b/service/aiproxy/relay/apitype/define.go deleted file mode 100644 index 212a1b6b1c3..00000000000 --- a/service/aiproxy/relay/apitype/define.go +++ /dev/null @@ -1,23 +0,0 @@ -package apitype - -const ( - OpenAI = iota - Anthropic - PaLM - Baidu - Zhipu - Ali - Xunfei - AIProxyLibrary - Tencent - Gemini - Ollama - AwsClaude - Coze - Cohere - Cloudflare - DeepL - VertexAI - - Dummy // this one is only for count, do not add any channel after this -) diff --git a/service/aiproxy/relay/channeltype/define.go b/service/aiproxy/relay/channeltype/define.go index cf82655c7dc..b1b761bf1f5 100644 --- a/service/aiproxy/relay/channeltype/define.go +++ b/service/aiproxy/relay/channeltype/define.go @@ -1,50 +1,84 @@ package channeltype -const ( - Unknown = iota - OpenAI - API2D - Azure - CloseAI - OpenAISB - OpenAIMax - OhMyGPT - Custom - Ails - AIProxy - PaLM - API2GPT - AIGC2D - Anthropic - Baidu - Zhipu - Ali - Xunfei - AI360 - OpenRouter - AIProxyLibrary - FastGPT - Tencent - Gemini - Moonshot - Baichuan - Minimax - Mistral - Groq - Ollama - LingYiWanWu - StepFun - AwsClaude - Coze - Cohere - DeepSeek - Cloudflare - DeepL - TogetherAI - Doubao - Novita - VertextAI - SiliconFlow - - Dummy +import ( + "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/ai360" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/ali" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/anthropic" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/aws" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/baichuan" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/baidu" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/baiduv2" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/cloudflare" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/cohere" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/coze" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/deepseek" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/doubao" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/doubaoaudio" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/gemini" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/groq" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/lingyiwanwu" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/minimax" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/mistral" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/moonshot" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/novita" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/ollama" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/siliconflow" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/stepfun" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/tencent" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/vertexai" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/xunfei" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/zhipu" ) + +var ChannelAdaptor = map[int]adaptor.Adaptor{ + 1: &openai.Adaptor{}, + // 3: &azure.Adaptor{}, + 13: &baiduv2.Adaptor{}, + 14: &anthropic.Adaptor{}, + 15: &baidu.Adaptor{}, + 16: &zhipu.Adaptor{}, + 17: &ali.Adaptor{}, + 18: &xunfei.Adaptor{}, + 19: &ai360.Adaptor{}, + 23: &tencent.Adaptor{}, + 24: &gemini.Adaptor{}, + 25: &moonshot.Adaptor{}, + 26: &baichuan.Adaptor{}, + 27: &minimax.Adaptor{}, + 28: &mistral.Adaptor{}, + 29: &groq.Adaptor{}, + 30: &ollama.Adaptor{}, + 31: &lingyiwanwu.Adaptor{}, + 32: &stepfun.Adaptor{}, + 33: &aws.Adaptor{}, + 34: &coze.Adaptor{}, + 35: &cohere.Adaptor{}, + 36: &deepseek.Adaptor{}, + 37: &cloudflare.Adaptor{}, + 40: &doubao.Adaptor{}, + 41: &novita.Adaptor{}, + 42: &vertexai.Adaptor{}, + 43: &siliconflow.Adaptor{}, + 44: &doubaoaudio.Adaptor{}, +} + +func GetAdaptor(channel int) (adaptor.Adaptor, bool) { + a, ok := ChannelAdaptor[channel] + return a, ok +} + +var ChannelNames = map[int]string{} + +func init() { + names := make(map[string]struct{}) + for i, adaptor := range ChannelAdaptor { + name := adaptor.GetChannelName() + if _, ok := names[name]; ok { + panic("duplicate channel name: " + name) + } + names[name] = struct{}{} + ChannelNames[i] = name + } +} diff --git a/service/aiproxy/relay/channeltype/helper.go b/service/aiproxy/relay/channeltype/helper.go deleted file mode 100644 index 87ad194a4c9..00000000000 --- a/service/aiproxy/relay/channeltype/helper.go +++ /dev/null @@ -1,42 +0,0 @@ -package channeltype - -import "github.com/labring/sealos/service/aiproxy/relay/apitype" - -func ToAPIType(channelType int) int { - switch channelType { - case Anthropic: - return apitype.Anthropic - case Baidu: - return apitype.Baidu - case PaLM: - return apitype.PaLM - case Zhipu: - return apitype.Zhipu - case Ali: - return apitype.Ali - case Xunfei: - return apitype.Xunfei - case AIProxyLibrary: - return apitype.AIProxyLibrary - case Tencent: - return apitype.Tencent - case Gemini: - return apitype.Gemini - case Ollama: - return apitype.Ollama - case AwsClaude: - return apitype.AwsClaude - case Coze: - return apitype.Coze - case Cohere: - return apitype.Cohere - case Cloudflare: - return apitype.Cloudflare - case DeepL: - return apitype.DeepL - case VertextAI: - return apitype.VertexAI - default: - return apitype.OpenAI - } -} diff --git a/service/aiproxy/relay/channeltype/url.go b/service/aiproxy/relay/channeltype/url.go deleted file mode 100644 index 5a485df485b..00000000000 --- a/service/aiproxy/relay/channeltype/url.go +++ /dev/null @@ -1,53 +0,0 @@ -package channeltype - -var ChannelBaseURLs = map[int]string{ - OpenAI: "https://api.openai.com", - API2D: "https://oa.api2d.net", - Azure: "", - CloseAI: "https://api.closeai-proxy.xyz", - OpenAISB: "https://api.openai-sb.com", - OpenAIMax: "https://api.openaimax.com", - OhMyGPT: "https://api.ohmygpt.com", - Custom: "", - Ails: "https://api.caipacity.com", - AIProxy: "https://api.aiproxy.io", - PaLM: "https://generativelanguage.googleapis.com", - API2GPT: "https://api.api2gpt.com", - AIGC2D: "https://api.aigc2d.com", - Anthropic: "https://api.anthropic.com", - Baidu: "https://aip.baidubce.com", - Zhipu: "https://open.bigmodel.cn", - Ali: "https://dashscope.aliyuncs.com", - Xunfei: "https://spark-api-open.xf-yun.com", - AI360: "https://ai.360.cn", - OpenRouter: "https://openrouter.ai/api", - AIProxyLibrary: "https://api.aiproxy.io", - FastGPT: "https://fastgpt.run/api/openapi", - Tencent: "https://hunyuan.tencentcloudapi.com", - Gemini: "https://generativelanguage.googleapis.com", - Moonshot: "https://api.moonshot.cn", - Baichuan: "https://api.baichuan-ai.com", - Minimax: "https://api.minimax.chat", - Mistral: "https://api.mistral.ai", - Groq: "https://api.groq.com/openai", - Ollama: "http://localhost:11434", - LingYiWanWu: "https://api.lingyiwanwu.com", - StepFun: "https://api.stepfun.com", - AwsClaude: "", - Coze: "https://api.coze.com", - Cohere: "https://api.cohere.ai", - DeepSeek: "https://api.deepseek.com", - Cloudflare: "https://api.cloudflare.com", - DeepL: "https://api-free.deepl.com", - TogetherAI: "https://api.together.xyz", - Doubao: "https://ark.cn-beijing.volces.com", - Novita: "https://api.novita.ai/v3/openai", - VertextAI: "", - SiliconFlow: "https://api.siliconflow.cn", -} - -func init() { - if len(ChannelBaseURLs) != Dummy-1 { - panic("channel base urls length not match") - } -} diff --git a/service/aiproxy/relay/channeltype/url_test.go b/service/aiproxy/relay/channeltype/url_test.go deleted file mode 100644 index 9406d8d912f..00000000000 --- a/service/aiproxy/relay/channeltype/url_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package channeltype - -import ( - "testing" - - "github.com/smartystreets/goconvey/convey" -) - -func TestChannelBaseURLs(t *testing.T) { - convey.Convey("channel base urls", t, func() { - convey.So(len(ChannelBaseURLs), convey.ShouldEqual, Dummy) - }) -} diff --git a/service/aiproxy/relay/controller/audio.go b/service/aiproxy/relay/controller/audio.go deleted file mode 100644 index fb1be0ad0ab..00000000000 --- a/service/aiproxy/relay/controller/audio.go +++ /dev/null @@ -1,153 +0,0 @@ -package controller - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "net/http" - - json "github.com/json-iterator/go" - "github.com/shopspring/decimal" - - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/balance" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/relay" - "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/meta" - relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" - billingprice "github.com/labring/sealos/service/aiproxy/relay/price" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" -) - -func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode { - meta := meta.GetByContext(c) - - channelType := c.GetInt(ctxkey.Channel) - group := c.GetString(ctxkey.Group) - - adaptor := relay.GetAdaptor(meta.APIType) - if adaptor == nil { - return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) - } - adaptor.Init(meta) - - meta.ActualModelName, _ = getMappedModelName(meta.OriginModelName, c.GetStringMapString(ctxkey.ModelMapping)) - - price, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName, channelType) - if !ok { - return openai.ErrorWrapper(fmt.Errorf("model price not found: %s", meta.OriginModelName), "model_price_not_found", http.StatusInternalServerError) - } - completionPrice, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName, channelType) - if !ok { - return openai.ErrorWrapper(fmt.Errorf("model price not found: %s", meta.OriginModelName), "model_price_not_found", http.StatusInternalServerError) - } - - var body io.ReadCloser - switch relayMode { - case relaymode.AudioSpeech: - var ttsRequest relaymodel.TextToSpeechRequest - err := common.UnmarshalBodyReusable(c, &ttsRequest) - if err != nil { - return openai.ErrorWrapper(err, "invalid_json", http.StatusBadRequest) - } - ttsRequest.Model = meta.ActualModelName - data, err := adaptor.ConvertTTSRequest(&ttsRequest) - if err != nil { - return openai.ErrorWrapper(err, "convert_tts_request_failed", http.StatusBadRequest) - } - jsonBody, err := json.Marshal(data) - if err != nil { - return openai.ErrorWrapper(err, "marshal_request_body_failed", http.StatusInternalServerError) - } - body = io.NopCloser(bytes.NewReader(jsonBody)) - meta.PromptTokens = openai.CountTokenText(ttsRequest.Input, meta.ActualModelName) - case relaymode.AudioTranscription: - var err error - body, err = adaptor.ConvertSTTRequest(c.Request) - if err != nil { - return openai.ErrorWrapper(err, "convert_stt_request_failed", http.StatusBadRequest) - } - default: - return openai.ErrorWrapper(fmt.Errorf("invalid relay mode: %d", relayMode), "invalid_relay_mode", http.StatusBadRequest) - } - - groupRemainBalance, postGroupConsumer, err := balance.Default.GetGroupRemainBalance(c.Request.Context(), group) - if err != nil { - logger.Errorf(c, "get group (%s) balance failed: %s", group, err) - return openai.ErrorWrapper( - fmt.Errorf("get group (%s) balance failed", group), - "get_group_balance_failed", - http.StatusInternalServerError, - ) - } - - preConsumedAmount := decimal.NewFromInt(int64(meta.PromptTokens)). - Mul(decimal.NewFromFloat(price)). - Div(decimal.NewFromInt(billingprice.PriceUnit)). - InexactFloat64() - // Check if group balance is enough - if groupRemainBalance < preConsumedAmount { - return openai.ErrorWrapper(errors.New("group balance is not enough"), "insufficient_group_balance", http.StatusForbidden) - } - - resp, err := adaptor.DoRequest(c, meta, body) - if err != nil { - logger.Errorf(c, "do request failed: %s", err.Error()) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - http.StatusInternalServerError, - c.Request.URL.Path, - nil, meta, price, completionPrice, err.Error(), - ) - return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) - } - - if isErrorHappened(meta, resp) { - err := RelayErrorHandler(resp, meta.Mode) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - resp.StatusCode, - c.Request.URL.Path, - nil, - meta, - price, - completionPrice, - err.String(), - ) - return err - } - - usage, respErr := adaptor.DoResponse(c, resp, meta) - if respErr != nil { - logger.Errorf(c, "do response failed: %s", respErr) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - respErr.StatusCode, - c.Request.URL.Path, - nil, meta, price, completionPrice, respErr.String(), - ) - return respErr - } - - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - resp.StatusCode, - c.Request.URL.Path, - usage, meta, price, completionPrice, "", - ) - - return nil -} diff --git a/service/aiproxy/relay/controller/helper.go b/service/aiproxy/relay/controller/helper.go index 084a940997b..f8b2d7579a0 100644 --- a/service/aiproxy/relay/controller/helper.go +++ b/service/aiproxy/relay/controller/helper.go @@ -1,59 +1,31 @@ package controller import ( + "bytes" "context" + "encoding/json" + "io" "net/http" - "strings" "sync" "github.com/gin-gonic/gin" "github.com/labring/sealos/service/aiproxy/common" "github.com/labring/sealos/service/aiproxy/common/balance" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/common/conv" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/model" + "github.com/labring/sealos/service/aiproxy/relay/adaptor" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" - "github.com/labring/sealos/service/aiproxy/relay/controller/validator" "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" billingprice "github.com/labring/sealos/service/aiproxy/relay/price" "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" "github.com/shopspring/decimal" ) var ConsumeWaitGroup sync.WaitGroup -func getAndValidateTextRequest(c *gin.Context, relayMode int) (*relaymodel.GeneralOpenAIRequest, error) { - textRequest := &relaymodel.GeneralOpenAIRequest{} - err := common.UnmarshalBodyReusable(c, textRequest) - if err != nil { - return nil, err - } - if relayMode == relaymode.Moderations && textRequest.Model == "" { - textRequest.Model = "text-moderation-latest" - } - if relayMode == relaymode.Embeddings && textRequest.Model == "" { - textRequest.Model = c.Param("model") - } - err = validator.ValidateTextRequest(textRequest, relayMode) - if err != nil { - return nil, err - } - return textRequest, nil -} - -func getPromptTokens(textRequest *relaymodel.GeneralOpenAIRequest, relayMode int) int { - switch relayMode { - case relaymode.ChatCompletions: - return openai.CountTokenMessages(textRequest.Messages, textRequest.Model) - case relaymode.Completions: - return openai.CountTokenInput(textRequest.Prompt, textRequest.Model) - case relaymode.Moderations: - return openai.CountTokenInput(textRequest.Input, textRequest.Model) - } - return 0 -} - type PreCheckGroupBalanceReq struct { PromptTokens int MaxTokens int @@ -76,9 +48,13 @@ func getPreConsumedAmount(req *PreCheckGroupBalanceReq) float64 { } func preCheckGroupBalance(ctx context.Context, req *PreCheckGroupBalanceReq, meta *meta.Meta) (bool, balance.PostGroupConsumer, error) { + if meta.IsChannelTest { + return true, nil, nil + } + preConsumedAmount := getPreConsumedAmount(req) - groupRemainBalance, postGroupConsumer, err := balance.Default.GetGroupRemainBalance(ctx, meta.Group) + groupRemainBalance, postGroupConsumer, err := balance.Default.GetGroupRemainBalance(ctx, meta.Group.ID) if err != nil { return false, nil, err } @@ -88,12 +64,47 @@ func preCheckGroupBalance(ctx context.Context, req *PreCheckGroupBalanceReq, met return true, postGroupConsumer, nil } -func postConsumeAmount(ctx context.Context, consumeWaitGroup *sync.WaitGroup, postGroupConsumer balance.PostGroupConsumer, code int, endpoint string, usage *relaymodel.Usage, meta *meta.Meta, price, completionPrice float64, content string) { +func postConsumeAmount( + ctx context.Context, + consumeWaitGroup *sync.WaitGroup, + postGroupConsumer balance.PostGroupConsumer, + code int, + endpoint string, + usage *relaymodel.Usage, + meta *meta.Meta, + price, + completionPrice float64, + content string, + requestDetail *model.RequestDetail, +) { defer consumeWaitGroup.Done() + if meta.IsChannelTest { + return + } + log := middleware.NewLogger() + middleware.SetLogFieldsFromMeta(meta, log.Data) if usage == nil { - err := model.BatchRecordConsume(ctx, meta.Group, code, meta.ChannelID, 0, 0, meta.OriginModelName, meta.TokenID, meta.TokenName, 0, price, completionPrice, endpoint, content) + err := model.BatchRecordConsume( + meta.RequestID, + meta.RequestAt, + meta.Group.ID, + code, + meta.Channel.ID, + 0, + 0, + meta.OriginModelName, + meta.Token.ID, + meta.Token.Name, + 0, + price, + completionPrice, + endpoint, + content, + meta.Mode, + requestDetail, + ) if err != nil { - logger.Error(ctx, "error batch record consume: "+err.Error()) + log.Error("error batch record consume: " + err.Error()) } return } @@ -102,53 +113,173 @@ func postConsumeAmount(ctx context.Context, consumeWaitGroup *sync.WaitGroup, po var amount float64 totalTokens := promptTokens + completionTokens if totalTokens != 0 { - // amount = (float64(promptTokens)*price + float64(completionTokens)*completionPrice) / billingPrice.PriceUnit - promptAmount := decimal.NewFromInt(int64(promptTokens)).Mul(decimal.NewFromFloat(price)).Div(decimal.NewFromInt(billingprice.PriceUnit)) - completionAmount := decimal.NewFromInt(int64(completionTokens)).Mul(decimal.NewFromFloat(completionPrice)).Div(decimal.NewFromInt(billingprice.PriceUnit)) + promptAmount := decimal. + NewFromInt(int64(promptTokens)). + Mul(decimal.NewFromFloat(price)). + Div(decimal.NewFromInt(billingprice.PriceUnit)) + completionAmount := decimal. + NewFromInt(int64(completionTokens)). + Mul(decimal.NewFromFloat(completionPrice)). + Div(decimal.NewFromInt(billingprice.PriceUnit)) amount = promptAmount.Add(completionAmount).InexactFloat64() if amount > 0 { - _amount, err := postGroupConsumer.PostGroupConsume(ctx, meta.TokenName, amount) + _amount, err := postGroupConsumer.PostGroupConsume(ctx, meta.Token.Name, amount) if err != nil { - logger.Error(ctx, "error consuming token remain amount: "+err.Error()) - err = model.CreateConsumeError(meta.Group, meta.TokenName, meta.OriginModelName, err.Error(), amount, meta.TokenID) + log.Error("error consuming token remain amount: " + err.Error()) + err = model.CreateConsumeError( + meta.RequestID, + meta.RequestAt, + meta.Group.ID, + meta.Token.Name, + meta.OriginModelName, + err.Error(), + amount, + meta.Token.ID, + ) if err != nil { - logger.Error(ctx, "failed to create consume error: "+err.Error()) + log.Error("failed to create consume error: " + err.Error()) } } else { amount = _amount } } } - err := model.BatchRecordConsume(ctx, meta.Group, code, meta.ChannelID, promptTokens, completionTokens, meta.OriginModelName, meta.TokenID, meta.TokenName, amount, price, completionPrice, endpoint, content) + err := model.BatchRecordConsume( + meta.RequestID, + meta.RequestAt, + meta.Group.ID, + code, + meta.Channel.ID, + promptTokens, + completionTokens, + meta.OriginModelName, + meta.Token.ID, + meta.Token.Name, + amount, + price, + completionPrice, + endpoint, + content, + meta.Mode, + requestDetail, + ) if err != nil { - logger.Error(ctx, "error batch record consume: "+err.Error()) + log.Error("error batch record consume: " + err.Error()) } } -func getMappedModelName(modelName string, mapping map[string]string) (string, bool) { - if mapping == nil { - return modelName, false - } - mappedModelName := mapping[modelName] - if mappedModelName != "" { - return mappedModelName, true +func isErrorHappened(resp *http.Response) bool { + if resp == nil { + return false } - return modelName, false + return resp.StatusCode != http.StatusOK } -func isErrorHappened(meta *meta.Meta, resp *http.Response) bool { - if resp == nil { - return meta.ChannelType != channeltype.AwsClaude +type responseWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + rw.body.Write(b) + return rw.ResponseWriter.Write(b) +} + +func (rw *responseWriter) WriteString(s string) (int, error) { + rw.body.WriteString(s) + return rw.ResponseWriter.WriteString(s) +} + +func DoHelper(a adaptor.Adaptor, c *gin.Context, meta *meta.Meta) (*relaymodel.Usage, *model.RequestDetail, *relaymodel.ErrorWithStatusCode) { + log := middleware.GetLogger(c) + + detail := model.RequestDetail{} + switch meta.Mode { + case relaymode.AudioTranscription, relaymode.AudioTranslation: + break + default: + reqBody, err := common.GetRequestBody(c.Request) + if err != nil { + return nil, nil, openai.ErrorWrapperWithMessage("get request body failed: "+err.Error(), "get_request_body_failed", http.StatusBadRequest) + } + detail.RequestBody = conv.BytesToString(reqBody) + } + + header, body, err := a.ConvertRequest(meta, c.Request) + if err != nil { + return nil, &detail, openai.ErrorWrapperWithMessage("convert request failed: "+err.Error(), "convert_request_failed", http.StatusBadRequest) + } + + fullRequestURL, err := a.GetRequestURL(meta) + if err != nil { + return nil, &detail, openai.ErrorWrapperWithMessage("get request url failed: "+err.Error(), "get_request_url_failed", http.StatusBadRequest) } - if resp.StatusCode != http.StatusOK { - return true + req, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, fullRequestURL, body) + if err != nil { + return nil, &detail, openai.ErrorWrapperWithMessage("new request failed: "+err.Error(), "new_request_failed", http.StatusBadRequest) } - if meta.ChannelType == channeltype.DeepL { - // skip stream check for deepl - return false + log.Debugf("request url: %s", fullRequestURL) + + contentType := req.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/json; charset=utf-8" + } + req.Header.Set("Content-Type", contentType) + for key, value := range header { + req.Header[key] = value + } + err = a.SetupRequestHeader(meta, c, req) + if err != nil { + return nil, &detail, openai.ErrorWrapperWithMessage("setup request header failed: "+err.Error(), "setup_request_header_failed", http.StatusBadRequest) + } + + resp, err := a.DoRequest(meta, c, req) + if err != nil { + return nil, &detail, openai.ErrorWrapperWithMessage("do request failed: "+err.Error(), "do_request_failed", http.StatusBadRequest) + } + + if isErrorHappened(resp) { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &detail, openai.ErrorWrapperWithMessage("read response body failed: "+err.Error(), "read_response_body_failed", http.StatusBadRequest) + } + detail.ResponseBody = conv.BytesToString(respBody) + resp.Body = io.NopCloser(bytes.NewReader(respBody)) + return nil, &detail, utils.RelayErrorHandler(meta, resp) + } + + rw := &responseWriter{ + ResponseWriter: c.Writer, + body: bytes.NewBuffer(nil), + } + rawWriter := c.Writer + defer func() { c.Writer = rawWriter }() + c.Writer = rw + + c.Header("Content-Type", resp.Header.Get("Content-Type")) + usage, relayErr := a.DoResponse(meta, c, resp) + detail.ResponseBody = conv.BytesToString(rw.body.Bytes()) + if relayErr != nil { + if detail.ResponseBody == "" { + respData, err := json.Marshal(gin.H{ + "error": relayErr.Error, + }) + if err != nil { + detail.ResponseBody = relayErr.Error.String() + } else { + detail.ResponseBody = conv.BytesToString(respData) + } + } + return nil, &detail, relayErr + } + if usage == nil { + usage = &relaymodel.Usage{ + PromptTokens: meta.PromptTokens, + TotalTokens: meta.PromptTokens, + } } - if meta.IsStream && strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { - return true + if usage.TotalTokens == 0 { + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens } - return false + return usage, &detail, nil } diff --git a/service/aiproxy/relay/controller/image.go b/service/aiproxy/relay/controller/image.go index 86c947a99e6..d9b514464a8 100644 --- a/service/aiproxy/relay/controller/image.go +++ b/service/aiproxy/relay/controller/image.go @@ -1,31 +1,23 @@ package controller import ( - "bytes" "context" "errors" "fmt" - "io" "net/http" - json "github.com/json-iterator/go" - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/balance" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/relay" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" "github.com/labring/sealos/service/aiproxy/relay/channeltype" "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" billingprice "github.com/labring/sealos/service/aiproxy/relay/price" - "github.com/shopspring/decimal" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) -func getImageRequest(c *gin.Context, _ int) (*relaymodel.ImageRequest, error) { - imageRequest := &relaymodel.ImageRequest{} - err := common.UnmarshalBodyReusable(c, imageRequest) +func getImageRequest(c *gin.Context) (*relaymodel.ImageRequest, error) { + imageRequest, err := utils.UnmarshalImageRequest(c.Request) if err != nil { return nil, err } @@ -33,179 +25,95 @@ func getImageRequest(c *gin.Context, _ int) (*relaymodel.ImageRequest, error) { imageRequest.N = 1 } if imageRequest.Size == "" { - imageRequest.Size = "1024x1024" - } - if imageRequest.Model == "" { - imageRequest.Model = "dall-e-2" + return nil, errors.New("size is required") } return imageRequest, nil } -func validateImageRequest(imageRequest *relaymodel.ImageRequest, _ *meta.Meta) *relaymodel.ErrorWithStatusCode { +func validateImageRequest(imageRequest *relaymodel.ImageRequest) *relaymodel.ErrorWithStatusCode { // check prompt length if imageRequest.Prompt == "" { return openai.ErrorWrapper(errors.New("prompt is required"), "prompt_missing", http.StatusBadRequest) } - // model validation - if !billingprice.IsValidImageSize(imageRequest.Model, imageRequest.Size) { - return openai.ErrorWrapper(errors.New("size not supported for this image model"), "size_not_supported", http.StatusBadRequest) - } - - if !billingprice.IsValidImagePromptLength(imageRequest.Model, len(imageRequest.Prompt)) { - return openai.ErrorWrapper(errors.New("prompt is too long"), "prompt_too_long", http.StatusBadRequest) - } - // Number of generated images validation - if !billingprice.IsWithinRange(imageRequest.Model, imageRequest.N) { - return openai.ErrorWrapper(errors.New("invalid value of n"), "n_not_within_range", http.StatusBadRequest) + if err := billingprice.ValidateImageMaxBatchSize(imageRequest.Model, imageRequest.N); err != nil { + return openai.ErrorWrapper(err, "n_not_within_range", http.StatusBadRequest) } return nil } -func getImageCostPrice(imageRequest *relaymodel.ImageRequest) (float64, error) { - if imageRequest == nil { - return 0, errors.New("imageRequest is nil") - } - imageCostPrice := billingprice.GetImageSizePrice(imageRequest.Model, imageRequest.Size) - if imageRequest.Quality == "hd" && imageRequest.Model == "dall-e-3" { - if imageRequest.Size == "1024x1024" { - imageCostPrice *= 2 - } else { - imageCostPrice *= 1.5 - } +func getImageCostPrice(modelName string, reqModel string, size string) (float64, error) { + imageCostPrice, ok := billingprice.GetImageSizePrice(modelName, reqModel, size) + if !ok { + return 0, fmt.Errorf("invalid image size: %s", size) } return imageCostPrice, nil } -func RelayImageHelper(c *gin.Context, _ int) *relaymodel.ErrorWithStatusCode { +func RelayImageHelper(meta *meta.Meta, c *gin.Context) *relaymodel.ErrorWithStatusCode { + log := middleware.GetLogger(c) ctx := c.Request.Context() - meta := meta.GetByContext(c) - imageRequest, err := getImageRequest(c, meta.Mode) + + imageRequest, err := getImageRequest(c) if err != nil { - logger.Errorf(ctx, "getImageRequest failed: %s", err.Error()) + log.Errorf("getImageRequest failed: %s", err.Error()) return openai.ErrorWrapper(err, "invalid_image_request", http.StatusBadRequest) } - // map model name - var isModelMapped bool - meta.OriginModelName = imageRequest.Model - imageRequest.Model, isModelMapped = getMappedModelName(imageRequest.Model, meta.ModelMapping) - meta.ActualModelName = imageRequest.Model + meta.PromptTokens = imageRequest.N - // model validation - bizErr := validateImageRequest(imageRequest, meta) + bizErr := validateImageRequest(imageRequest) if bizErr != nil { return bizErr } - imageCostPrice, err := getImageCostPrice(imageRequest) + imageCostPrice, err := getImageCostPrice(meta.OriginModelName, meta.ActualModelName, imageRequest.Size) if err != nil { return openai.ErrorWrapper(err, "get_image_cost_price_failed", http.StatusInternalServerError) } - // Convert the original image model - imageRequest.Model, _ = getMappedModelName(imageRequest.Model, billingprice.GetImageOriginModelName()) - c.Set("response_format", imageRequest.ResponseFormat) - - adaptor := relay.GetAdaptor(meta.APIType) - if adaptor == nil { - return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) - } - adaptor.Init(meta) - - var requestBody io.Reader - switch meta.ChannelType { - case channeltype.Ali, - channeltype.Baidu, - channeltype.Zhipu: - finalRequest, err := adaptor.ConvertImageRequest(imageRequest) - if err != nil { - return openai.ErrorWrapper(err, "convert_image_request_failed", http.StatusInternalServerError) - } - jsonStr, err := json.Marshal(finalRequest) - if err != nil { - return openai.ErrorWrapper(err, "marshal_image_request_failed", http.StatusInternalServerError) - } - requestBody = bytes.NewReader(jsonStr) - default: - if isModelMapped || meta.ChannelType == channeltype.Azure { // make Azure channel request body - jsonStr, err := json.Marshal(imageRequest) - if err != nil { - return openai.ErrorWrapper(err, "marshal_image_request_failed", http.StatusInternalServerError) - } - requestBody = bytes.NewReader(jsonStr) - } else { - requestBody = c.Request.Body - } + adaptor, ok := channeltype.GetAdaptor(meta.Channel.Type) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("invalid channel type: %d", meta.Channel.Type), "invalid_channel_type", http.StatusBadRequest) } - groupRemainBalance, postGroupConsumer, err := balance.Default.GetGroupRemainBalance(ctx, meta.Group) + ok, postGroupConsumer, err := preCheckGroupBalance(ctx, &PreCheckGroupBalanceReq{ + PromptTokens: meta.PromptTokens, + Price: imageCostPrice, + }, meta) if err != nil { - logger.Errorf(ctx, "get group (%s) balance failed: %s", meta.Group, err) - return openai.ErrorWrapper( - fmt.Errorf("get group (%s) balance failed", meta.Group), - "get_group_remain_balance_failed", - http.StatusInternalServerError, - ) - } - - amount := decimal.NewFromFloat(imageCostPrice).Mul(decimal.NewFromInt(int64(imageRequest.N))).InexactFloat64() - - if groupRemainBalance-amount < 0 { + log.Errorf("get group (%s) balance failed: %v", meta.Group.ID, err) return openai.ErrorWrapper( - errors.New("group balance is not enough"), - "insufficient_group_balance", - http.StatusForbidden, - ) - } - - // do request - resp, err := adaptor.DoRequest(c, meta, requestBody) - if err != nil { - logger.Errorf(ctx, "do request failed: %s", err.Error()) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, + fmt.Errorf("get group (%s) balance failed", meta.Group.ID), + "get_group_quota_failed", http.StatusInternalServerError, - c.Request.URL.Path, nil, meta, imageCostPrice, 0, err.Error(), ) - return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) } - - if isErrorHappened(meta, resp) { - err := RelayErrorHandler(resp, meta.Mode) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - resp.StatusCode, - c.Request.URL.Path, - nil, - meta, - imageCostPrice, - 0, - err.String(), - ) - return err + if !ok { + return openai.ErrorWrapper(errors.New("group balance is not enough"), "insufficient_group_balance", http.StatusForbidden) } // do response - _, respErr := adaptor.DoResponse(c, resp, meta) + usage, detail, respErr := DoHelper(adaptor, c, meta) if respErr != nil { - logger.Errorf(ctx, "do response failed: %s", respErr) + if detail != nil { + log.Errorf("do image failed: %s\nrequest detail:\n%s\nresponse detail:\n%s", respErr, detail.RequestBody, detail.ResponseBody) + } else { + log.Errorf("do image failed: %s", respErr) + } ConsumeWaitGroup.Add(1) go postConsumeAmount(context.Background(), &ConsumeWaitGroup, postGroupConsumer, respErr.StatusCode, c.Request.URL.Path, - nil, + usage, meta, imageCostPrice, 0, respErr.String(), + detail, ) return respErr } @@ -214,9 +122,14 @@ func RelayImageHelper(c *gin.Context, _ int) *relaymodel.ErrorWithStatusCode { go postConsumeAmount(context.Background(), &ConsumeWaitGroup, postGroupConsumer, - resp.StatusCode, + http.StatusOK, c.Request.URL.Path, - nil, meta, imageCostPrice, 0, imageRequest.Size, + usage, + meta, + imageCostPrice, + 0, + imageRequest.Size, + nil, ) return nil diff --git a/service/aiproxy/relay/controller/rerank.go b/service/aiproxy/relay/controller/rerank.go index 3654b9ef900..70001623ff1 100644 --- a/service/aiproxy/relay/controller/rerank.go +++ b/service/aiproxy/relay/controller/rerank.go @@ -1,48 +1,36 @@ package controller import ( - "bytes" "context" "errors" "fmt" - "io" "net/http" "strings" "github.com/gin-gonic/gin" - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/relay" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/channeltype" "github.com/labring/sealos/service/aiproxy/relay/meta" relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" billingprice "github.com/labring/sealos/service/aiproxy/relay/price" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) -func RerankHelper(c *gin.Context) *relaymodel.ErrorWithStatusCode { +func RerankHelper(meta *meta.Meta, c *gin.Context) *relaymodel.ErrorWithStatusCode { + log := middleware.GetLogger(c) ctx := c.Request.Context() - meta := meta.GetByContext(c) + rerankRequest, err := getRerankRequest(c) if err != nil { - logger.Errorf(ctx, "get rerank request failed: %s", err.Error()) + log.Errorf("get rerank request failed: %s", err.Error()) return openai.ErrorWrapper(err, "invalid_rerank_request", http.StatusBadRequest) } - meta.OriginModelName = rerankRequest.Model - rerankRequest.Model, _ = getMappedModelName(rerankRequest.Model, meta.ModelMapping) - meta.ActualModelName = rerankRequest.Model - - price, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName, meta.ChannelType) + price, completionPrice, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName) if !ok { return openai.ErrorWrapper(fmt.Errorf("model price not found: %s", meta.OriginModelName), "model_price_not_found", http.StatusInternalServerError) } - completionPrice, ok := billingprice.GetCompletionPrice(meta.OriginModelName, meta.ActualModelName, meta.ChannelType) - if !ok { - return openai.ErrorWrapper(fmt.Errorf("completion price not found: %s", meta.OriginModelName), "completion_price_not_found", http.StatusInternalServerError) - } meta.PromptTokens = rerankPromptTokens(rerankRequest) @@ -51,9 +39,9 @@ func RerankHelper(c *gin.Context) *relaymodel.ErrorWithStatusCode { Price: price, }, meta) if err != nil { - logger.Errorf(ctx, "get group (%s) balance failed: %s", meta.Group, err) + log.Errorf("get group (%s) balance failed: %v", meta.Group.ID, err) return openai.ErrorWrapper( - fmt.Errorf("get group (%s) balance failed", meta.Group), + fmt.Errorf("get group (%s) balance failed", meta.Group.ID), "get_group_quota_failed", http.StatusInternalServerError, ) @@ -62,58 +50,30 @@ func RerankHelper(c *gin.Context) *relaymodel.ErrorWithStatusCode { return openai.ErrorWrapper(errors.New("group balance is not enough"), "insufficient_group_balance", http.StatusForbidden) } - adaptor := relay.GetAdaptor(meta.APIType) - if adaptor == nil { - return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) - } - - requestBody, err := getRerankRequestBody(c, meta, rerankRequest, adaptor) - if err != nil { - logger.Errorf(ctx, "get rerank request body failed: %s", err.Error()) - return openai.ErrorWrapper(err, "invalid_rerank_request", http.StatusBadRequest) + adaptor, ok := channeltype.GetAdaptor(meta.Channel.Type) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("invalid channel type: %d", meta.Channel.Type), "invalid_channel_type", http.StatusBadRequest) } - resp, err := adaptor.DoRequest(c, meta, requestBody) - if err != nil { - logger.Errorf(ctx, "do rerank request failed: %s", err.Error()) + usage, detail, respErr := DoHelper(adaptor, c, meta) + if respErr != nil { + if detail != nil { + log.Errorf("do rerank failed: %s\nrequest detail:\n%s\nresponse detail:\n%s", respErr, detail.RequestBody, detail.ResponseBody) + } else { + log.Errorf("do rerank failed: %s", respErr) + } ConsumeWaitGroup.Add(1) go postConsumeAmount(context.Background(), &ConsumeWaitGroup, postGroupConsumer, http.StatusInternalServerError, c.Request.URL.Path, - nil, meta, price, completionPrice, err.Error(), - ) - return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) - } - - if isErrorHappened(meta, resp) { - err := RelayErrorHandler(resp, relaymode.Rerank) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - resp.StatusCode, - c.Request.URL.Path, - nil, + usage, meta, price, completionPrice, - err.String(), - ) - return err - } - - usage, respErr := adaptor.DoResponse(c, resp, meta) - if respErr != nil { - logger.Errorf(ctx, "do rerank response failed: %+v", respErr) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - http.StatusInternalServerError, - c.Request.URL.Path, - usage, meta, price, completionPrice, respErr.String(), + respErr.String(), + detail, ) return respErr } @@ -124,15 +84,19 @@ func RerankHelper(c *gin.Context) *relaymodel.ErrorWithStatusCode { postGroupConsumer, http.StatusOK, c.Request.URL.Path, - usage, meta, price, completionPrice, "", + usage, + meta, + price, + completionPrice, + "", + nil, ) return nil } func getRerankRequest(c *gin.Context) (*relaymodel.RerankRequest, error) { - rerankRequest := &relaymodel.RerankRequest{} - err := common.UnmarshalBodyReusable(c, rerankRequest) + rerankRequest, err := utils.UnmarshalRerankRequest(c.Request) if err != nil { return nil, err } @@ -149,14 +113,6 @@ func getRerankRequest(c *gin.Context) (*relaymodel.RerankRequest, error) { return rerankRequest, nil } -func getRerankRequestBody(_ *gin.Context, _ *meta.Meta, textRequest *relaymodel.RerankRequest, _ adaptor.Adaptor) (io.Reader, error) { - jsonData, err := json.Marshal(textRequest) - if err != nil { - return nil, err - } - return bytes.NewReader(jsonData), nil -} - func rerankPromptTokens(rerankRequest *relaymodel.RerankRequest) int { return len(rerankRequest.Query) + len(strings.Join(rerankRequest.Documents, "")) } diff --git a/service/aiproxy/relay/controller/stt.go b/service/aiproxy/relay/controller/stt.go new file mode 100644 index 00000000000..030de04add4 --- /dev/null +++ b/service/aiproxy/relay/controller/stt.go @@ -0,0 +1,86 @@ +package controller + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/channeltype" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + billingprice "github.com/labring/sealos/service/aiproxy/relay/price" +) + +func RelaySTTHelper(meta *meta.Meta, c *gin.Context) *relaymodel.ErrorWithStatusCode { + log := middleware.GetLogger(c) + ctx := c.Request.Context() + + adaptor, ok := channeltype.GetAdaptor(meta.Channel.Type) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("invalid channel type: %d", meta.Channel.Type), "invalid_channel_type", http.StatusBadRequest) + } + + price, completionPrice, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("model price not found: %s", meta.OriginModelName), "model_price_not_found", http.StatusInternalServerError) + } + + ok, postGroupConsumer, err := preCheckGroupBalance(ctx, &PreCheckGroupBalanceReq{ + PromptTokens: meta.PromptTokens, + Price: price, + }, meta) + if err != nil { + log.Errorf("get group (%s) balance failed: %v", meta.Group.ID, err) + return openai.ErrorWrapper( + fmt.Errorf("get group (%s) balance failed", meta.Group.ID), + "get_group_quota_failed", + http.StatusInternalServerError, + ) + } + if !ok { + return openai.ErrorWrapper(errors.New("group balance is not enough"), "insufficient_group_balance", http.StatusForbidden) + } + + usage, detail, respErr := DoHelper(adaptor, c, meta) + if respErr != nil { + if detail != nil { + log.Errorf("do stt failed: %s\nrequest detail:\n%s\nresponse detail:\n%s", respErr, detail.RequestBody, detail.ResponseBody) + } else { + log.Errorf("do stt failed: %s", respErr) + } + ConsumeWaitGroup.Add(1) + go postConsumeAmount(context.Background(), + &ConsumeWaitGroup, + postGroupConsumer, + respErr.StatusCode, + c.Request.URL.Path, + usage, + meta, + price, + completionPrice, + respErr.String(), + detail, + ) + return respErr + } + + ConsumeWaitGroup.Add(1) + go postConsumeAmount(context.Background(), + &ConsumeWaitGroup, + postGroupConsumer, + http.StatusOK, + c.Request.URL.Path, + usage, + meta, + price, + completionPrice, + "", + nil, + ) + + return nil +} diff --git a/service/aiproxy/relay/controller/text.go b/service/aiproxy/relay/controller/text.go index b510e05950b..50ffafb485d 100644 --- a/service/aiproxy/relay/controller/text.go +++ b/service/aiproxy/relay/controller/text.go @@ -1,50 +1,38 @@ package controller import ( - "bytes" "context" "errors" "fmt" - "io" "net/http" "github.com/gin-gonic/gin" - json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/logger" - "github.com/labring/sealos/service/aiproxy/relay" - "github.com/labring/sealos/service/aiproxy/relay/adaptor" + "github.com/labring/sealos/service/aiproxy/middleware" "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/channeltype" "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" billingprice "github.com/labring/sealos/service/aiproxy/relay/price" + "github.com/labring/sealos/service/aiproxy/relay/utils" ) -func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { +func RelayTextHelper(meta *meta.Meta, c *gin.Context) *model.ErrorWithStatusCode { + log := middleware.GetLogger(c) ctx := c.Request.Context() - meta := meta.GetByContext(c) - textRequest, err := getAndValidateTextRequest(c, meta.Mode) + + textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request) if err != nil { - logger.Errorf(ctx, "get and validate text request failed: %s", err.Error()) + log.Errorf("get and validate text request failed: %s", err.Error()) return openai.ErrorWrapper(err, "invalid_text_request", http.StatusBadRequest) } - meta.IsStream = textRequest.Stream - - // map model name - meta.OriginModelName = textRequest.Model - textRequest.Model, _ = getMappedModelName(textRequest.Model, meta.ModelMapping) - meta.ActualModelName = textRequest.Model // get model price - price, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName, meta.ChannelType) + price, completionPrice, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName) if !ok { return openai.ErrorWrapper(fmt.Errorf("model price not found: %s", meta.OriginModelName), "model_price_not_found", http.StatusInternalServerError) } - completionPrice, ok := billingprice.GetCompletionPrice(meta.OriginModelName, meta.ActualModelName, meta.ChannelType) - if !ok { - return openai.ErrorWrapper(fmt.Errorf("completion price not found: %s", meta.OriginModelName), "completion_price_not_found", http.StatusInternalServerError) - } // pre-consume balance - promptTokens := getPromptTokens(textRequest, meta.Mode) + promptTokens := openai.GetPromptTokens(meta, textRequest) meta.PromptTokens = promptTokens ok, postGroupConsumer, err := preCheckGroupBalance(ctx, &PreCheckGroupBalanceReq{ PromptTokens: promptTokens, @@ -52,9 +40,9 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { Price: price, }, meta) if err != nil { - logger.Errorf(ctx, "get group (%s) balance failed: %s", meta.Group, err) + log.Errorf("get group (%s) balance failed: %v", meta.Group.ID, err) return openai.ErrorWrapper( - fmt.Errorf("get group (%s) balance failed", meta.Group), + fmt.Errorf("get group (%s) balance failed", meta.Group.ID), "get_group_quota_failed", http.StatusInternalServerError, ) @@ -63,56 +51,19 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { return openai.ErrorWrapper(errors.New("group balance is not enough"), "insufficient_group_balance", http.StatusForbidden) } - adaptor := relay.GetAdaptor(meta.APIType) - if adaptor == nil { - return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) - } - adaptor.Init(meta) - - // get request body - requestBody, err := getRequestBody(c, meta, textRequest, adaptor) - if err != nil { - logger.Errorf(ctx, "get request body failed: %s", err.Error()) - return openai.ErrorWrapper(err, "convert_request_failed", http.StatusInternalServerError) - } - logger.Debugf(ctx, "converted request: \n%s", requestBody) - - // do request - resp, err := adaptor.DoRequest(c, meta, requestBody) - if err != nil { - logger.Errorf(ctx, "do request failed: %s", err.Error()) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - http.StatusInternalServerError, - c.Request.URL.Path, - nil, meta, price, completionPrice, err.Error(), - ) - return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) - } - - if isErrorHappened(meta, resp) { - err := RelayErrorHandler(resp, meta.Mode) - ConsumeWaitGroup.Add(1) - go postConsumeAmount(context.Background(), - &ConsumeWaitGroup, - postGroupConsumer, - resp.StatusCode, - c.Request.URL.Path, - nil, - meta, - price, - completionPrice, - err.String(), - ) - return err + adaptor, ok := channeltype.GetAdaptor(meta.Channel.Type) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("invalid channel type: %d", meta.Channel.Type), "invalid_channel_type", http.StatusBadRequest) } // do response - usage, respErr := adaptor.DoResponse(c, resp, meta) + usage, detail, respErr := DoHelper(adaptor, c, meta) if respErr != nil { - logger.Errorf(ctx, "do response failed: %s", respErr) + if detail != nil { + log.Errorf("do text failed: %s\nrequest detail:\n%s\nresponse detail:\n%s", respErr, detail.RequestBody, detail.ResponseBody) + } else { + log.Errorf("do text failed: %s", respErr) + } ConsumeWaitGroup.Add(1) go postConsumeAmount(context.Background(), &ConsumeWaitGroup, @@ -124,6 +75,7 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { price, completionPrice, respErr.String(), + detail, ) return respErr } @@ -132,21 +84,14 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { go postConsumeAmount(context.Background(), &ConsumeWaitGroup, postGroupConsumer, - resp.StatusCode, + http.StatusOK, c.Request.URL.Path, - usage, meta, price, completionPrice, "", + usage, + meta, + price, + completionPrice, + "", + nil, ) return nil } - -func getRequestBody(c *gin.Context, meta *meta.Meta, textRequest *model.GeneralOpenAIRequest, adaptor adaptor.Adaptor) (io.Reader, error) { - convertedRequest, err := adaptor.ConvertRequest(c, meta.Mode, textRequest) - if err != nil { - return nil, err - } - jsonData, err := json.Marshal(convertedRequest) - if err != nil { - return nil, err - } - return bytes.NewReader(jsonData), nil -} diff --git a/service/aiproxy/relay/controller/tts.go b/service/aiproxy/relay/controller/tts.go new file mode 100644 index 00000000000..df669b3b768 --- /dev/null +++ b/service/aiproxy/relay/controller/tts.go @@ -0,0 +1,93 @@ +package controller + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/labring/sealos/service/aiproxy/middleware" + "github.com/labring/sealos/service/aiproxy/relay/adaptor/openai" + "github.com/labring/sealos/service/aiproxy/relay/channeltype" + "github.com/labring/sealos/service/aiproxy/relay/meta" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + billingprice "github.com/labring/sealos/service/aiproxy/relay/price" + "github.com/labring/sealos/service/aiproxy/relay/utils" +) + +func RelayTTSHelper(meta *meta.Meta, c *gin.Context) *relaymodel.ErrorWithStatusCode { + log := middleware.GetLogger(c) + ctx := c.Request.Context() + + adaptor, ok := channeltype.GetAdaptor(meta.Channel.Type) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("invalid channel type: %d", meta.Channel.Type), "invalid_channel_type", http.StatusBadRequest) + } + + price, completionPrice, ok := billingprice.GetModelPrice(meta.OriginModelName, meta.ActualModelName) + if !ok { + return openai.ErrorWrapper(fmt.Errorf("model price not found: %s", meta.OriginModelName), "model_price_not_found", http.StatusInternalServerError) + } + + ttsRequest, err := utils.UnmarshalTTSRequest(c.Request) + if err != nil { + return openai.ErrorWrapper(err, "invalid_json", http.StatusBadRequest) + } + meta.PromptTokens = openai.CountTokenText(ttsRequest.Input, meta.ActualModelName) + + ok, postGroupConsumer, err := preCheckGroupBalance(ctx, &PreCheckGroupBalanceReq{ + PromptTokens: meta.PromptTokens, + Price: price, + }, meta) + if err != nil { + log.Errorf("get group (%s) balance failed: %v", meta.Group.ID, err) + return openai.ErrorWrapper( + fmt.Errorf("get group (%s) balance failed", meta.Group.ID), + "get_group_quota_failed", + http.StatusInternalServerError, + ) + } + if !ok { + return openai.ErrorWrapper(errors.New("group balance is not enough"), "insufficient_group_balance", http.StatusForbidden) + } + + usage, detail, respErr := DoHelper(adaptor, c, meta) + if respErr != nil { + if detail != nil { + log.Errorf("do tts failed: %s\nrequest detail:\n%s\nresponse detail:\n%s", respErr, detail.RequestBody, detail.ResponseBody) + } else { + log.Errorf("do tts failed: %s", respErr) + } + ConsumeWaitGroup.Add(1) + go postConsumeAmount(context.Background(), + &ConsumeWaitGroup, + postGroupConsumer, + respErr.StatusCode, + c.Request.URL.Path, + usage, + meta, + price, + completionPrice, + respErr.String(), + detail, + ) + return respErr + } + + ConsumeWaitGroup.Add(1) + go postConsumeAmount(context.Background(), + &ConsumeWaitGroup, + postGroupConsumer, + http.StatusOK, + c.Request.URL.Path, + usage, + meta, + price, + completionPrice, + "", + nil, + ) + + return nil +} diff --git a/service/aiproxy/relay/controller/validator/validation.go b/service/aiproxy/relay/controller/validator/validation.go deleted file mode 100644 index 4f29c84a86d..00000000000 --- a/service/aiproxy/relay/controller/validator/validation.go +++ /dev/null @@ -1,38 +0,0 @@ -package validator - -import ( - "errors" - "math" - - "github.com/labring/sealos/service/aiproxy/relay/model" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" -) - -func ValidateTextRequest(textRequest *model.GeneralOpenAIRequest, relayMode int) error { - if textRequest.MaxTokens < 0 || textRequest.MaxTokens > math.MaxInt32/2 { - return errors.New("max_tokens is invalid") - } - if textRequest.Model == "" { - return errors.New("model is required") - } - switch relayMode { - case relaymode.Completions: - if textRequest.Prompt == "" { - return errors.New("field prompt is required") - } - case relaymode.ChatCompletions: - if len(textRequest.Messages) == 0 { - return errors.New("field messages is required") - } - case relaymode.Embeddings: - case relaymode.Moderations: - if textRequest.Input == "" { - return errors.New("field input is required") - } - case relaymode.Edits: - if textRequest.Instruction == "" { - return errors.New("field instruction is required") - } - } - return nil -} diff --git a/service/aiproxy/relay/meta/meta.go b/service/aiproxy/relay/meta/meta.go new file mode 100644 index 00000000000..f5159df7474 --- /dev/null +++ b/service/aiproxy/relay/meta/meta.go @@ -0,0 +1,162 @@ +package meta + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" + "github.com/labring/sealos/service/aiproxy/model" +) + +type ChannelMeta struct { + Config model.ChannelConfig + Name string + BaseURL string + Key string + ID int + Type int +} + +type Meta struct { + values map[string]any + Channel *ChannelMeta + Group *model.GroupCache + Token *model.TokenCache + + RequestAt time.Time + RequestID string + OriginModelName string + ActualModelName string + Mode int + PromptTokens int + IsChannelTest bool +} + +type Option func(meta *Meta) + +func WithChannelTest(isChannelTest bool) Option { + return func(meta *Meta) { + meta.IsChannelTest = isChannelTest + } +} + +func WithRequestID(requestID string) Option { + return func(meta *Meta) { + meta.RequestID = requestID + } +} + +func WithRequestAt(requestAt time.Time) Option { + return func(meta *Meta) { + meta.RequestAt = requestAt + } +} + +func WithGroup(group *model.GroupCache) Option { + return func(meta *Meta) { + meta.Group = group + } +} + +func WithToken(token *model.TokenCache) Option { + return func(meta *Meta) { + meta.Token = token + } +} + +func NewMeta(channel *model.Channel, mode int, modelName string, opts ...Option) *Meta { + meta := Meta{ + values: make(map[string]any), + Mode: mode, + OriginModelName: modelName, + RequestAt: time.Now(), + } + + for _, opt := range opts { + opt(&meta) + } + + meta.Reset(channel) + + return &meta +} + +func (m *Meta) Reset(channel *model.Channel) { + m.Channel = &ChannelMeta{ + Config: channel.Config, + Name: channel.Name, + BaseURL: channel.BaseURL, + Key: channel.Key, + ID: channel.ID, + Type: channel.Type, + } + m.ActualModelName, _ = GetMappedModelName(m.OriginModelName, channel.ModelMapping) + m.ClearValues() +} + +func (m *Meta) ClearValues() { + clear(m.values) +} + +func (m *Meta) Set(key string, value any) { + m.values[key] = value +} + +func (m *Meta) Get(key string) (any, bool) { + v, ok := m.values[key] + return v, ok +} + +func (m *Meta) Delete(key string) { + delete(m.values, key) +} + +func (m *Meta) MustGet(key string) any { + v, ok := m.Get(key) + if !ok { + panic(fmt.Sprintf("meta key %s not found", key)) + } + return v +} + +func (m *Meta) GetString(key string) string { + v, ok := m.Get(key) + if !ok { + return "" + } + s, _ := v.(string) + return s +} + +func (m *Meta) GetBool(key string) bool { + if v, ok := m.Get(key); ok { + return v.(bool) + } + return false +} + +func (m *Meta) AwsClient() *bedrockruntime.Client { + if v, ok := m.Get("awsClient"); ok { + return v.(*bedrockruntime.Client) + } + awsClient := bedrockruntime.New(bedrockruntime.Options{ + Region: m.Channel.Config.Region, + Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(m.Channel.Config.AK, m.Channel.Config.SK, "")), + }) + m.Set("awsClient", awsClient) + return awsClient +} + +//nolint:unparam +func GetMappedModelName(modelName string, mapping map[string]string) (string, bool) { + if len(modelName) == 0 { + return modelName, false + } + mappedModelName := mapping[modelName] + if mappedModelName != "" { + return mappedModelName, true + } + return modelName, false +} diff --git a/service/aiproxy/relay/meta/relay_meta.go b/service/aiproxy/relay/meta/relay_meta.go deleted file mode 100644 index f07c862636d..00000000000 --- a/service/aiproxy/relay/meta/relay_meta.go +++ /dev/null @@ -1,53 +0,0 @@ -package meta - -import ( - "github.com/gin-gonic/gin" - "github.com/labring/sealos/service/aiproxy/common/ctxkey" - "github.com/labring/sealos/service/aiproxy/model" - "github.com/labring/sealos/service/aiproxy/relay/channeltype" - "github.com/labring/sealos/service/aiproxy/relay/relaymode" -) - -type Meta struct { - ModelMapping map[string]string - Config model.ChannelConfig - APIKey string - OriginModelName string - TokenName string - Group string - RequestURLPath string - BaseURL string - ActualModelName string - ChannelID int - ChannelType int - APIType int - Mode int - TokenID int - PromptTokens int - IsStream bool -} - -func GetByContext(c *gin.Context) *Meta { - meta := Meta{ - Mode: relaymode.GetByPath(c.Request.URL.Path), - ChannelType: c.GetInt(ctxkey.Channel), - ChannelID: c.GetInt(ctxkey.ChannelID), - TokenID: c.GetInt(ctxkey.TokenID), - TokenName: c.GetString(ctxkey.TokenName), - Group: c.GetString(ctxkey.Group), - ModelMapping: c.GetStringMapString(ctxkey.ModelMapping), - OriginModelName: c.GetString(ctxkey.RequestModel), - BaseURL: c.GetString(ctxkey.BaseURL), - APIKey: c.GetString(ctxkey.APIKey), - RequestURLPath: c.Request.URL.String(), - } - cfg, ok := c.Get(ctxkey.Config) - if ok { - meta.Config = cfg.(model.ChannelConfig) - } - if meta.BaseURL == "" { - meta.BaseURL = channeltype.ChannelBaseURLs[meta.ChannelType] - } - meta.APIType = channeltype.ToAPIType(meta.ChannelType) - return &meta -} diff --git a/service/aiproxy/relay/model/general.go b/service/aiproxy/relay/model/general.go index 8038f5ab751..50250238523 100644 --- a/service/aiproxy/relay/model/general.go +++ b/service/aiproxy/relay/model/general.go @@ -52,8 +52,8 @@ type GeneralOpenAIRequest struct { User string `json:"user,omitempty"` Size string `json:"size,omitempty"` Modalities []string `json:"modalities,omitempty"` - Messages []Message `json:"messages,omitempty"` - Tools []Tool `json:"tools,omitempty"` + Messages []*Message `json:"messages,omitempty"` + Tools []*Tool `json:"tools,omitempty"` N int `json:"n,omitempty"` Dimensions int `json:"dimensions,omitempty"` Seed float64 `json:"seed,omitempty"` diff --git a/service/aiproxy/relay/model/image.go b/service/aiproxy/relay/model/image.go index 1ba51218c36..945e67bb5a9 100644 --- a/service/aiproxy/relay/model/image.go +++ b/service/aiproxy/relay/model/image.go @@ -2,7 +2,7 @@ package model type ImageRequest struct { Model string `json:"model"` - Prompt string `binding:"required" json:"prompt"` + Prompt string `json:"prompt"` Size string `json:"size,omitempty"` Quality string `json:"quality,omitempty"` ResponseFormat string `json:"response_format,omitempty"` diff --git a/service/aiproxy/relay/model/message.go b/service/aiproxy/relay/model/message.go index 4e5def601e7..eefe4e02964 100644 --- a/service/aiproxy/relay/model/message.go +++ b/service/aiproxy/relay/model/message.go @@ -1,43 +1,54 @@ package model +import "strings" + type Message struct { Content any `json:"content,omitempty"` Name *string `json:"name,omitempty"` Role string `json:"role,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` - ToolCalls []Tool `json:"tool_calls,omitempty"` + ToolCalls []*Tool `json:"tool_calls,omitempty"` } -func (m Message) IsStringContent() bool { +func (m *Message) IsStringContent() bool { _, ok := m.Content.(string) return ok } -func (m Message) StringContent() string { +func (m *Message) ToStringContentMessage() { + if m.IsStringContent() { + return + } + m.Content = m.StringContent() +} + +func (m *Message) StringContent() string { content, ok := m.Content.(string) if ok { return content } contentList, ok := m.Content.([]any) - if ok { - var contentStr string - for _, contentItem := range contentList { - contentMap, ok := contentItem.(map[string]any) - if !ok { - continue - } - if contentMap["type"] == ContentTypeText { - if subStr, ok := contentMap["text"].(string); ok { - contentStr += subStr - } + if !ok { + return "" + } + + var strBuilder strings.Builder + for _, contentItem := range contentList { + contentMap, ok := contentItem.(map[string]any) + if !ok { + continue + } + if contentMap["type"] == ContentTypeText { + if subStr, ok := contentMap["text"].(string); ok { + strBuilder.WriteString(subStr) + strBuilder.WriteString("\n") } } - return contentStr } - return "" + return strBuilder.String() } -func (m Message) ParseContent() []MessageContent { +func (m *Message) ParseContent() []MessageContent { var contentList []MessageContent content, ok := m.Content.(string) if ok { diff --git a/service/aiproxy/relay/model/rerank.go b/service/aiproxy/relay/model/rerank.go index a58b3cab225..1bbdf5dc54e 100644 --- a/service/aiproxy/relay/model/rerank.go +++ b/service/aiproxy/relay/model/rerank.go @@ -15,9 +15,9 @@ type Document struct { } type RerankResult struct { - Document Document `json:"document"` - Index int `json:"index"` - RelevanceScore float64 `json:"relevance_score"` + Document *Document `json:"document,omitempty"` + Index int `json:"index"` + RelevanceScore float64 `json:"relevance_score"` } type RerankMetaTokens struct { @@ -26,12 +26,12 @@ type RerankMetaTokens struct { } type RerankMeta struct { - Tokens *RerankMetaTokens `json:"tokens"` - Model string `json:"model"` + Tokens *RerankMetaTokens `json:"tokens,omitempty"` + Model string `json:"model,omitempty"` } type RerankResponse struct { - Meta RerankMeta `json:"meta"` - ID string `json:"id"` - Result []RerankResult `json:"result"` + Meta RerankMeta `json:"meta"` + ID string `json:"id"` + Result []*RerankResult `json:"result"` } diff --git a/service/aiproxy/relay/price/image.go b/service/aiproxy/relay/price/image.go index cd40bc01db7..19bcde3f7b2 100644 --- a/service/aiproxy/relay/price/image.go +++ b/service/aiproxy/relay/price/image.go @@ -1,108 +1,51 @@ package price -// 单个图片的价格 -var imageSizePrices = map[string]map[string]float64{ - "dall-e-2": { - "256x256": 1, - "512x512": 1.125, - "1024x1024": 1.25, - }, - "dall-e-3": { - "1024x1024": 1, - "1024x1792": 2, - "1792x1024": 2, - }, - "ali-stable-diffusion-xl": { - "512x1024": 1, - "1024x768": 1, - "1024x1024": 1, - "576x1024": 1, - "1024x576": 1, - }, - "ali-stable-diffusion-v1.5": { - "512x1024": 1, - "1024x768": 1, - "1024x1024": 1, - "576x1024": 1, - "1024x576": 1, - }, - "wanx-v1": { - "1024x1024": 1, - "720x1280": 1, - "1280x720": 1, - }, - "step-1x-medium": { - "256x256": 1, - "512x512": 1, - "768x768": 1, - "1024x1024": 1, - "1280x800": 1, - "800x1280": 1, - }, -} - -var imageGenerationAmounts = map[string][2]int{ - "dall-e-2": {1, 10}, - "dall-e-3": {1, 1}, // OpenAI allows n=1 currently. - "ali-stable-diffusion-xl": {1, 4}, // Ali - "ali-stable-diffusion-v1.5": {1, 4}, // Ali - "wanx-v1": {1, 4}, // Ali - "cogview-3": {1, 1}, - "step-1x-medium": {1, 1}, -} +import ( + "errors" + "fmt" -var imagePromptLengthLimitations = map[string]int{ - "dall-e-2": 1000, - "dall-e-3": 4000, - "ali-stable-diffusion-xl": 4000, - "ali-stable-diffusion-v1.5": 4000, - "wanx-v1": 4000, - "cogview-3": 833, - "step-1x-medium": 4000, -} + "github.com/labring/sealos/service/aiproxy/common/config" + "github.com/labring/sealos/service/aiproxy/model" +) -var imageOriginModelName = map[string]string{ - "ali-stable-diffusion-xl": "stable-diffusion-xl", - "ali-stable-diffusion-v1.5": "stable-diffusion-v1.5", -} - -func GetImageOriginModelName() map[string]string { - return imageOriginModelName -} - -func IsValidImageSize(model string, size string) bool { - if !GetBillingEnabled() { - return true +func GetImageSizePrice(model string, reqModel string, size string) (float64, bool) { + if !config.GetBillingEnabled() { + return 0, false } - if model == "cogview-3" || imageSizePrices[model] == nil { - return true + if price, ok := getImageSizePrice(model, size); ok { + return price, true } - _, ok := imageSizePrices[model][size] - return ok -} - -func IsValidImagePromptLength(model string, promptLength int) bool { - if !GetBillingEnabled() { - return true + if price, ok := getImageSizePrice(reqModel, size); ok { + return price, true } - maxPromptLength, ok := imagePromptLengthLimitations[model] - return !ok || promptLength <= maxPromptLength + return 0, false } -func IsWithinRange(element string, value int) bool { - if !GetBillingEnabled() { - return true +func getImageSizePrice(modelName string, size string) (float64, bool) { + modelConfig, ok := model.CacheGetModelConfig(modelName) + if !ok { + return 0, false } - amounts, ok := imageGenerationAmounts[element] - return !ok || (value >= amounts[0] && value <= amounts[1]) + if len(modelConfig.ImagePrices) == 0 { + return 0, true + } + price, ok := modelConfig.ImagePrices[size] + return price, ok } -func GetImageSizePrice(model string, size string) float64 { - if !GetBillingEnabled() { - return 0 +func ValidateImageMaxBatchSize(modelName string, batchSize int) error { + if batchSize <= 1 { + return nil + } + modelConfig, ok := model.CacheGetModelConfig(modelName) + if !ok { + return errors.New("model not found") + } + if modelConfig.ImageMaxBatchSize <= 0 { + return nil } - if price, ok := imageSizePrices[model][size]; ok { - return price + if batchSize > modelConfig.ImageMaxBatchSize { + return fmt.Errorf("batch size %d is greater than the maximum batch size %d", batchSize, modelConfig.ImageMaxBatchSize) } - return 1 + return nil } diff --git a/service/aiproxy/relay/price/model.go b/service/aiproxy/relay/price/model.go index ed6104a5c35..09cda1bd708 100644 --- a/service/aiproxy/relay/price/model.go +++ b/service/aiproxy/relay/price/model.go @@ -1,14 +1,8 @@ package price import ( - "fmt" - "sync" - "sync/atomic" - - json "github.com/json-iterator/go" - - "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/common/config" + "github.com/labring/sealos/service/aiproxy/model" ) const ( @@ -21,186 +15,22 @@ const ( // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf // https://openai.com/pricing // 价格单位:人民币/1K tokens -var ( - modelPrice = map[string]float64{} - completionPrice = map[string]float64{} - modelPriceMu sync.RWMutex - completionPriceMu sync.RWMutex -) - -var ( - DefaultModelPrice map[string]float64 - DefaultCompletionPrice map[string]float64 -) - -func init() { - DefaultModelPrice = make(map[string]float64) - modelPriceMu.RLock() - for k, v := range modelPrice { - DefaultModelPrice[k] = v - } - modelPriceMu.RUnlock() - - DefaultCompletionPrice = make(map[string]float64) - completionPriceMu.RLock() - for k, v := range completionPrice { - DefaultCompletionPrice[k] = v - } - completionPriceMu.RUnlock() -} - -func AddNewMissingPrice(oldPrice string) string { - newPrice := make(map[string]float64) - err := json.Unmarshal(conv.StringToBytes(oldPrice), &newPrice) - if err != nil { - logger.SysError("error unmarshalling old price: " + err.Error()) - return oldPrice - } - for k, v := range DefaultModelPrice { - if _, ok := newPrice[k]; !ok { - newPrice[k] = v - } - } - jsonBytes, err := json.Marshal(newPrice) - if err != nil { - logger.SysError("error marshalling new price: " + err.Error()) - return oldPrice - } - return conv.BytesToString(jsonBytes) -} - -func ModelPrice2JSONString() string { - modelPriceMu.RLock() - jsonBytes, err := json.Marshal(modelPrice) - modelPriceMu.RUnlock() - if err != nil { - logger.SysError("error marshalling model price: " + err.Error()) - } - return conv.BytesToString(jsonBytes) -} - -var billingEnabled atomic.Bool - -func init() { - billingEnabled.Store(true) -} - -func GetBillingEnabled() bool { - return billingEnabled.Load() -} - -func SetBillingEnabled(enabled bool) { - billingEnabled.Store(enabled) -} - -func UpdateModelPriceByJSONString(jsonStr string) error { - newModelPrice := make(map[string]float64) - err := json.Unmarshal(conv.StringToBytes(jsonStr), &newModelPrice) - if err != nil { - logger.SysError("error unmarshalling model price: " + err.Error()) - return err - } - modelPriceMu.Lock() - modelPrice = newModelPrice - modelPriceMu.Unlock() - return nil -} - -func GetModelPrice(mapedName string, reqModel string, channelType int) (float64, bool) { - if !GetBillingEnabled() { - return 0, true - } - price, ok := getModelPrice(mapedName, channelType) - if !ok && reqModel != "" { - price, ok = getModelPrice(reqModel, channelType) - } - return price, ok -} - -func getModelPrice(modelName string, channelType int) (float64, bool) { - model := fmt.Sprintf("%s(%d)", modelName, channelType) - modelPriceMu.RLock() - defer modelPriceMu.RUnlock() - price, ok := modelPrice[model] - if ok { - return price, true - } - if price, ok := DefaultModelPrice[model]; ok { - return price, true - } - price, ok = modelPrice[modelName] - if ok { - return price, true - } - if price, ok := DefaultModelPrice[modelName]; ok { - return price, true - } - return 0, false -} -func CompletionPrice2JSONString() string { - completionPriceMu.RLock() - jsonBytes, err := json.Marshal(completionPrice) - completionPriceMu.RUnlock() - if err != nil { - logger.SysError("error marshalling completion price: " + err.Error()) +func GetModelPrice(mapedName string, reqModel string) (float64, float64, bool) { + if !config.GetBillingEnabled() { + return 0, 0, true } - return conv.BytesToString(jsonBytes) -} - -func UpdateCompletionPriceByJSONString(jsonStr string) error { - newCompletionPrice := make(map[string]float64) - err := json.Unmarshal(conv.StringToBytes(jsonStr), &newCompletionPrice) - if err != nil { - logger.SysError("error unmarshalling completion price: " + err.Error()) - return err - } - completionPriceMu.Lock() - completionPrice = newCompletionPrice - completionPriceMu.Unlock() - return nil -} - -func GetCompletionPrice(name string, reqModel string, channelType int) (float64, bool) { - if !GetBillingEnabled() { - return 0, true - } - price, ok := getCompletionPrice(name, channelType) + price, completionPrice, ok := getModelPrice(mapedName) if !ok && reqModel != "" { - price, ok = getCompletionPrice(reqModel, channelType) + price, completionPrice, ok = getModelPrice(reqModel) } - return price, ok + return price, completionPrice, ok } -func getCompletionPrice(name string, channelType int) (float64, bool) { - model := fmt.Sprintf("%s(%d)", name, channelType) - completionPriceMu.RLock() - defer completionPriceMu.RUnlock() - price, ok := completionPrice[model] - if ok { - return price, true - } - if price, ok := DefaultCompletionPrice[model]; ok { - return price, true +func getModelPrice(modelName string) (float64, float64, bool) { + modelConfig, ok := model.CacheGetModelConfig(modelName) + if !ok { + return 0, 0, false } - price, ok = completionPrice[name] - if ok { - return price, true - } - if price, ok := DefaultCompletionPrice[name]; ok { - return price, true - } - return getModelPrice(name, channelType) -} - -func GetModelPriceMap() map[string]float64 { - modelPriceMu.RLock() - defer modelPriceMu.RUnlock() - return modelPrice -} - -func GetCompletionPriceMap() map[string]float64 { - completionPriceMu.RLock() - defer completionPriceMu.RUnlock() - return completionPrice + return modelConfig.InputPrice, modelConfig.OutputPrice, true } diff --git a/service/aiproxy/relay/controller/error.go b/service/aiproxy/relay/utils/error.go similarity index 81% rename from service/aiproxy/relay/controller/error.go rename to service/aiproxy/relay/utils/error.go index 7f6433e2644..c70ce99eeaf 100644 --- a/service/aiproxy/relay/controller/error.go +++ b/service/aiproxy/relay/utils/error.go @@ -1,4 +1,4 @@ -package controller +package utils import ( "fmt" @@ -8,9 +8,8 @@ import ( "strings" json "github.com/json-iterator/go" - "github.com/labring/sealos/service/aiproxy/common/config" "github.com/labring/sealos/service/aiproxy/common/conv" - "github.com/labring/sealos/service/aiproxy/common/logger" + "github.com/labring/sealos/service/aiproxy/relay/meta" "github.com/labring/sealos/service/aiproxy/relay/model" "github.com/labring/sealos/service/aiproxy/relay/relaymode" ) @@ -56,18 +55,23 @@ func (e GeneralErrorResponse) ToMessage() string { return "" } -func RelayErrorHandler(resp *http.Response, relayMode int) *model.ErrorWithStatusCode { +const ( + ErrorTypeUpstream = "upstream_error" + ErrorCodeBadResponse = "bad_response" +) + +func RelayErrorHandler(meta *meta.Meta, resp *http.Response) *model.ErrorWithStatusCode { if resp == nil { return &model.ErrorWithStatusCode{ StatusCode: 500, Error: model.Error{ Message: "resp is nil", - Type: "upstream_error", - Code: "bad_response", + Type: ErrorTypeUpstream, + Code: ErrorCodeBadResponse, }, } } - switch relayMode { + switch meta.Mode { case relaymode.Rerank: return RerankErrorHandler(resp) default: @@ -83,8 +87,8 @@ func RerankErrorHandler(resp *http.Response) *model.ErrorWithStatusCode { StatusCode: resp.StatusCode, Error: model.Error{ Message: err.Error(), - Type: "upstream_error", - Code: "bad_response", + Type: ErrorTypeUpstream, + Code: ErrorCodeBadResponse, }, } } @@ -93,8 +97,8 @@ func RerankErrorHandler(resp *http.Response) *model.ErrorWithStatusCode { StatusCode: resp.StatusCode, Error: model.Error{ Message: trimmedRespBody, - Type: "upstream_error", - Code: "bad_response", + Type: ErrorTypeUpstream, + Code: ErrorCodeBadResponse, }, } } @@ -107,6 +111,8 @@ func RelayDefaultErrorHanlder(resp *http.Response) *model.ErrorWithStatusCode { StatusCode: resp.StatusCode, Error: model.Error{ Message: err.Error(), + Type: ErrorTypeUpstream, + Code: ErrorCodeBadResponse, }, } } @@ -115,8 +121,8 @@ func RelayDefaultErrorHanlder(resp *http.Response) *model.ErrorWithStatusCode { StatusCode: resp.StatusCode, Error: model.Error{ Message: "", - Type: "upstream_error", - Code: "bad_response_status_code", + Type: ErrorTypeUpstream, + Code: ErrorCodeBadResponse, Param: strconv.Itoa(resp.StatusCode), }, } @@ -125,9 +131,6 @@ func RelayDefaultErrorHanlder(resp *http.Response) *model.ErrorWithStatusCode { if err != nil { return ErrorWithStatusCode } - if config.DebugEnabled { - logger.SysLogf("error happened, status code: %d, response: \n%+v", resp.StatusCode, errResponse) - } if errResponse.Error.Message != "" { // OpenAI format error, so we override the default one ErrorWithStatusCode.Error = errResponse.Error diff --git a/service/aiproxy/relay/utils/testreq.go b/service/aiproxy/relay/utils/testreq.go new file mode 100644 index 00000000000..23ff2dfaac9 --- /dev/null +++ b/service/aiproxy/relay/utils/testreq.go @@ -0,0 +1,191 @@ +package utils + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strconv" + + "github.com/labring/sealos/service/aiproxy/model" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" + "github.com/labring/sealos/service/aiproxy/relay/relaymode" +) + +type UnsupportedModelTypeError struct { + ModelType string +} + +func (e *UnsupportedModelTypeError) Error() string { + return fmt.Sprintf("model type '%s' not supported", e.ModelType) +} + +func NewErrUnsupportedModelType(modelType string) *UnsupportedModelTypeError { + return &UnsupportedModelTypeError{ModelType: modelType} +} + +func BuildRequest(modelName string) (io.Reader, int, error) { + modelConfig, ok := model.CacheGetModelConfig(modelName) + if !ok { + return nil, relaymode.Unknown, errors.New(modelName + " model config not found") + } + switch modelConfig.Type { + case relaymode.ChatCompletions: + body, err := BuildChatCompletionRequest(modelName) + if err != nil { + return nil, relaymode.Unknown, err + } + return body, relaymode.ChatCompletions, nil + case relaymode.Completions: + return nil, relaymode.Unknown, NewErrUnsupportedModelType("completions") + case relaymode.Embeddings: + body, err := BuildEmbeddingsRequest(modelName) + if err != nil { + return nil, relaymode.Unknown, err + } + return body, relaymode.Embeddings, nil + case relaymode.Moderations: + body, err := BuildModerationsRequest(modelName) + if err != nil { + return nil, relaymode.Unknown, err + } + return body, relaymode.Moderations, nil + case relaymode.ImagesGenerations: + body, err := BuildImagesGenerationsRequest(modelConfig) + if err != nil { + return nil, relaymode.Unknown, err + } + return body, relaymode.ImagesGenerations, nil + case relaymode.Edits: + return nil, relaymode.Unknown, NewErrUnsupportedModelType("edits") + case relaymode.AudioSpeech: + body, err := BuildAudioSpeechRequest(modelName) + if err != nil { + return nil, relaymode.Unknown, err + } + return body, relaymode.AudioSpeech, nil + case relaymode.AudioTranscription: + return nil, relaymode.Unknown, NewErrUnsupportedModelType("audio transcription") + case relaymode.AudioTranslation: + return nil, relaymode.Unknown, NewErrUnsupportedModelType("audio translation") + case relaymode.Rerank: + body, err := BuildRerankRequest(modelName) + if err != nil { + return nil, relaymode.Unknown, err + } + return body, relaymode.Rerank, nil + default: + return nil, relaymode.Unknown, NewErrUnsupportedModelType(strconv.Itoa(modelConfig.Type)) + } +} + +func BuildChatCompletionRequest(model string) (io.Reader, error) { + testRequest := &relaymodel.GeneralOpenAIRequest{ + MaxTokens: 2, + Model: model, + Messages: []*relaymodel.Message{ + { + Role: "user", + Content: "hi", + }, + }, + } + jsonBytes, err := json.Marshal(testRequest) + if err != nil { + return nil, err + } + return bytes.NewReader(jsonBytes), nil +} + +func BuildEmbeddingsRequest(model string) (io.Reader, error) { + embeddingsRequest := &relaymodel.GeneralOpenAIRequest{ + Model: model, + Input: "hi", + } + jsonBytes, err := json.Marshal(embeddingsRequest) + if err != nil { + return nil, err + } + return bytes.NewReader(jsonBytes), nil +} + +func BuildModerationsRequest(model string) (io.Reader, error) { + moderationsRequest := &relaymodel.GeneralOpenAIRequest{ + Model: model, + Input: "hi", + } + jsonBytes, err := json.Marshal(moderationsRequest) + if err != nil { + return nil, err + } + return bytes.NewReader(jsonBytes), nil +} + +func BuildImagesGenerationsRequest(modelConfig *model.ModelConfig) (io.Reader, error) { + imagesGenerationsRequest := &relaymodel.GeneralOpenAIRequest{ + Model: modelConfig.Model, + Prompt: "hi", + Size: "1024x1024", + } + for size := range modelConfig.ImagePrices { + imagesGenerationsRequest.Size = size + break + } + jsonBytes, err := json.Marshal(imagesGenerationsRequest) + if err != nil { + return nil, err + } + return bytes.NewReader(jsonBytes), nil +} + +func BuildAudioSpeechRequest(model string) (io.Reader, error) { + audioSpeechRequest := &relaymodel.GeneralOpenAIRequest{ + Model: model, + Input: "hi", + } + jsonBytes, err := json.Marshal(audioSpeechRequest) + if err != nil { + return nil, err + } + return bytes.NewReader(jsonBytes), nil +} + +func BuildRerankRequest(model string) (io.Reader, error) { + rerankRequest := &relaymodel.RerankRequest{ + Model: model, + Query: "hi", + Documents: []string{"hi"}, + } + jsonBytes, err := json.Marshal(rerankRequest) + if err != nil { + return nil, err + } + return bytes.NewReader(jsonBytes), nil +} + +func BuildModeDefaultPath(mode int) string { + switch mode { + case relaymode.ChatCompletions: + return "/v1/chat/completions" + case relaymode.Completions: + return "/v1/completions" + case relaymode.Embeddings: + return "/v1/embeddings" + case relaymode.Moderations: + return "/v1/moderations" + case relaymode.ImagesGenerations: + return "/v1/images/generations" + case relaymode.Edits: + return "/v1/edits" + case relaymode.AudioSpeech: + return "/v1/audio/speech" + case relaymode.AudioTranscription: + return "/v1/audio/transcriptions" + case relaymode.AudioTranslation: + return "/v1/audio/translations" + case relaymode.Rerank: + return "/v1/rerank" + } + return "" +} diff --git a/service/aiproxy/relay/utils/utils.go b/service/aiproxy/relay/utils/utils.go new file mode 100644 index 00000000000..aebd317a721 --- /dev/null +++ b/service/aiproxy/relay/utils/utils.go @@ -0,0 +1,67 @@ +package utils + +import ( + "net/http" + "strings" + + "github.com/labring/sealos/service/aiproxy/common" + "github.com/labring/sealos/service/aiproxy/common/client" + relaymodel "github.com/labring/sealos/service/aiproxy/relay/model" +) + +func UnmarshalGeneralOpenAIRequest(req *http.Request) (*relaymodel.GeneralOpenAIRequest, error) { + var request relaymodel.GeneralOpenAIRequest + err := common.UnmarshalBodyReusable(req, &request) + if err != nil { + return nil, err + } + return &request, nil +} + +func UnmarshalImageRequest(req *http.Request) (*relaymodel.ImageRequest, error) { + var request relaymodel.ImageRequest + err := common.UnmarshalBodyReusable(req, &request) + if err != nil { + return nil, err + } + return &request, nil +} + +func UnmarshalRerankRequest(req *http.Request) (*relaymodel.RerankRequest, error) { + var request relaymodel.RerankRequest + err := common.UnmarshalBodyReusable(req, &request) + if err != nil { + return nil, err + } + return &request, nil +} + +func UnmarshalTTSRequest(req *http.Request) (*relaymodel.TextToSpeechRequest, error) { + var request relaymodel.TextToSpeechRequest + err := common.UnmarshalBodyReusable(req, &request) + if err != nil { + return nil, err + } + return &request, nil +} + +func UnmarshalMap(req *http.Request) (map[string]any, error) { + var request map[string]any + err := common.UnmarshalBodyReusable(req, &request) + if err != nil { + return nil, err + } + return request, nil +} + +func DoRequest(req *http.Request) (*http.Response, error) { + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} + +func IsStreamResponse(resp *http.Response) bool { + return strings.Contains(resp.Header.Get("Content-Type"), "event-stream") +} diff --git a/service/aiproxy/router/api.go b/service/aiproxy/router/api.go index 164af29df45..0e6d36486c2 100644 --- a/service/aiproxy/router/api.go +++ b/service/aiproxy/router/api.go @@ -21,23 +21,23 @@ func SetAPIRouter(router *gin.Engine) { apiRouter := api.Group("") apiRouter.Use(middleware.AdminAuth) { - apiRouter.GET("/models", controller.BuiltinModels) - apiRouter.GET("/models/price", controller.ModelPrice) - apiRouter.GET("/models/enabled", controller.EnabledModels) - apiRouter.GET("/models/enabled/price", controller.EnabledModelsAndPrice) - apiRouter.GET("/models/enabled/channel", controller.EnabledType2Models) - apiRouter.GET("/models/enabled/channel/price", controller.EnabledType2ModelsAndPrice) - apiRouter.GET("/models/enabled/default", controller.ChannelDefaultModels) - apiRouter.GET("/models/enabled/default/:type", controller.ChannelDefaultModelsByType) - apiRouter.GET("/models/enabled/mapping/default", controller.ChannelDefaultModelMapping) - apiRouter.GET("/models/enabled/mapping/default/:type", controller.ChannelDefaultModelMappingByType) - apiRouter.GET("/models/enabled/all/default", controller.ChannelDefaultModelsAndMapping) - apiRouter.GET("/models/enabled/all/default/:type", controller.ChannelDefaultModelsAndMappingByType) + modelsRoute := apiRouter.Group("/models") + { + modelsRoute.GET("/builtin", controller.BuiltinModels) + modelsRoute.GET("/builtin/channel", controller.ChannelBuiltinModels) + modelsRoute.GET("/builtin/channel/:type", controller.ChannelBuiltinModelsByType) + modelsRoute.GET("/enabled", controller.EnabledModels) + modelsRoute.GET("/enabled/channel", controller.ChannelEnabledModels) + modelsRoute.GET("/enabled/channel/:type", controller.ChannelEnabledModelsByType) + modelsRoute.GET("/default", controller.ChannelDefaultModelsAndMapping) + modelsRoute.GET("/default/:type", controller.ChannelDefaultModelsAndMappingByType) + } groupsRoute := apiRouter.Group("/groups") { groupsRoute.GET("/", controller.GetGroups) groupsRoute.GET("/search", controller.SearchGroups) + groupsRoute.POST("/batch_delete", controller.DeleteGroups) } groupRoute := apiRouter.Group("/group") { @@ -47,31 +47,37 @@ func SetAPIRouter(router *gin.Engine) { groupRoute.POST("/:id/status", controller.UpdateGroupStatus) groupRoute.POST("/:id/qpm", controller.UpdateGroupQPM) } + optionRoute := apiRouter.Group("/option") { optionRoute.GET("/", controller.GetOptions) optionRoute.PUT("/", controller.UpdateOption) optionRoute.PUT("/batch", controller.UpdateOptions) } + channelsRoute := apiRouter.Group("/channels") { channelsRoute.GET("/", controller.GetChannels) channelsRoute.GET("/all", controller.GetAllChannels) + channelsRoute.GET("/type_names", controller.ChannelTypeNames) channelsRoute.POST("/", controller.AddChannels) channelsRoute.GET("/search", controller.SearchChannels) - channelsRoute.GET("/test", controller.TestChannels) channelsRoute.GET("/update_balance", controller.UpdateAllChannelsBalance) + channelsRoute.POST("/batch_delete", controller.DeleteChannels) + channelsRoute.GET("/test", controller.TestAllChannels) } channelRoute := apiRouter.Group("/channel") { channelRoute.GET("/:id", controller.GetChannel) channelRoute.POST("/", controller.AddChannel) - channelRoute.PUT("/", controller.UpdateChannel) + channelRoute.PUT("/:id", controller.UpdateChannel) channelRoute.POST("/:id/status", controller.UpdateChannelStatus) channelRoute.DELETE("/:id", controller.DeleteChannel) - channelRoute.GET("/test/:id", controller.TestChannel) - channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance) + channelRoute.GET("/:id/test", controller.TestChannelModels) + channelRoute.GET("/:id/test/:model", controller.TestChannel) + channelRoute.GET("/:id/update_balance", controller.UpdateChannelBalance) } + tokensRoute := apiRouter.Group("/tokens") { tokensRoute.GET("/", controller.GetTokens) @@ -81,10 +87,12 @@ func SetAPIRouter(router *gin.Engine) { tokensRoute.POST("/:id/name", controller.UpdateTokenName) tokensRoute.DELETE("/:id", controller.DeleteToken) tokensRoute.GET("/search", controller.SearchTokens) + tokensRoute.POST("/batch_delete", controller.DeleteTokens) } tokenRoute := apiRouter.Group("/token") { tokenRoute.GET("/:group/search", controller.SearchGroupTokens) + tokenRoute.POST("/:group/batch_delete", controller.DeleteGroupTokens) tokenRoute.GET("/:group", controller.GetGroupTokens) tokenRoute.GET("/:group/:id", controller.GetGroupToken) tokenRoute.POST("/:group", controller.AddToken) @@ -93,11 +101,11 @@ func SetAPIRouter(router *gin.Engine) { tokenRoute.POST("/:group/:id/name", controller.UpdateGroupTokenName) tokenRoute.DELETE("/:group/:id", controller.DeleteGroupToken) } + logsRoute := apiRouter.Group("/logs") { logsRoute.GET("/", controller.GetLogs) logsRoute.DELETE("/", controller.DeleteHistoryLogs) - logsRoute.GET("/stat", controller.GetLogsStat) logsRoute.GET("/search", controller.SearchLogs) logsRoute.GET("/consume_error", controller.SearchConsumeError) } @@ -106,5 +114,21 @@ func SetAPIRouter(router *gin.Engine) { logRoute.GET("/:group/search", controller.SearchGroupLogs) logRoute.GET("/:group", controller.GetGroupLogs) } + + modelConfigsRoute := apiRouter.Group("/model_configs") + { + modelConfigsRoute.GET("/", controller.GetModelConfigs) + modelConfigsRoute.GET("/search", controller.SearchModelConfigs) + modelConfigsRoute.GET("/all", controller.GetAllModelConfigs) + modelConfigsRoute.POST("/contains", controller.GetModelConfigsByModelsContains) + modelConfigsRoute.POST("/", controller.SaveModelConfigs) + modelConfigsRoute.POST("/batch_delete", controller.DeleteModelConfigs) + } + modelConfigRoute := apiRouter.Group("/model_config") + { + modelConfigRoute.GET("/:model", controller.GetModelConfig) + modelConfigRoute.POST("/", controller.SaveModelConfig) + modelConfigRoute.DELETE("/:model", controller.DeleteModelConfig) + } } } diff --git a/service/aiproxy/router/relay.go b/service/aiproxy/router/relay.go index 20886eb9ddf..a15ccaa91dc 100644 --- a/service/aiproxy/router/relay.go +++ b/service/aiproxy/router/relay.go @@ -24,7 +24,7 @@ func SetRelayRouter(router *gin.Engine) { dashboardRouter.GET("/billing/usage", controller.GetUsage) } relayV1Router := router.Group("/v1") - relayV1Router.Use(middleware.RelayPanicRecover, middleware.TokenAuth, middleware.Distribute) + relayV1Router.Use(middleware.TokenAuth, middleware.Distribute) { relayV1Router.POST("/completions", controller.Relay) relayV1Router.POST("/chat/completions", controller.Relay) diff --git a/service/exceptionmonitor/api/api.go b/service/exceptionmonitor/api/api.go index 1bc49255969..3deafe83826 100644 --- a/service/exceptionmonitor/api/api.go +++ b/service/exceptionmonitor/api/api.go @@ -22,33 +22,72 @@ type QueryResult struct { } `json:"data"` } +type Info struct { + DatabaseClusterName string + Namespace string + DebtLevel string + DatabaseType string + Events string + Reason string + NotificationType string + DiskUsage string + CPUUsage string + MemUsage string + PerformanceType string + ExceptionType string + ExceptionStatus string + RecoveryStatus string + ExceptionStatusTime string + RecoveryTime string + DatabaseClusterUID string + FeishuWebHook string + //struct + FeishuInfo []map[string]interface{} +} + +type NameSpaceQuota struct { + NameSpace string + CPULimit string + MemLimit string + GPULimit string + EphemeralStorageLimit string + ObjectStorageLimit string + NodePortLimit string + StorageLimit string + CPUUsage string + MemUsage string + GPUUsage string + EphemeralStorageUsage string + ObjectStorageUsage string + NodePortUsage string + StorageUsage string +} + const ( StatusDeleting = "Deleting" StatusCreating = "Creating" StatusStopping = "Stopping" StatusStopped = "Stopped" StatusRunning = "Running" - StatusUpdating = "Updating" + //StatusUpdating = "Updating" StatusUnknown = "" MonitorTypeALL = "all" + DiskChinese = "磁盘" + MemoryChinese = "内存" + CPUChinese = "CPU" ) var ( - ClientSet *kubernetes.Clientset - DynamicClient *dynamic.DynamicClient - // records the last database status - LastDatabaseClusterStatus = make(map[string]string) - // record the debt ns - ExceptionDatabaseMap = make(map[string]bool) - FeishuWebHookMap = make(map[string]string) + ClientSet *kubernetes.Clientset + DynamicClient *dynamic.DynamicClient DebtNamespaceMap = make(map[string]bool) DiskFullNamespaceMap = make(map[string]bool) - DiskMonitorNamespaceMap = make(map[string]bool) - CPUMonitorNamespaceMap = make(map[string]bool) - MemMonitorNamespaceMap = make(map[string]bool) + CPUNotificationInfoMap = make(map[string]*Info) + MemNotificationInfoMap = make(map[string]*Info) + DiskNotificationInfoMap = make(map[string]*Info) LastBackupStatusMap = make(map[string]string) IsSendBackupStatusMap = make(map[string]string) - DatabaseNamespaceMap = make(map[string]string) + DatabaseNotificationInfoMap = make(map[string]*Info) ExceededQuotaException = "exceeded quota" DiskException = "Writing to log file failed" OwnerLabel = "user.sealos.io/owner" @@ -65,6 +104,7 @@ var ( CPUMemMonitor bool BackupMonitor bool QuotaMonitor bool + CockroachMonitor bool DatabaseDiskMonitorThreshold float64 DatabaseExceptionMonitorThreshold float64 DatabaseCPUMonitorThreshold float64 @@ -72,6 +112,8 @@ var ( QuotaThreshold float64 APPID string APPSECRET string + GlobalCockroachURI string + LocalCockroachURI string DatabaseStatusMessageIDMap = make(map[string]string) DatabaseDiskMessageIDMap = make(map[string]string) DatabaseCPUMessageIDMap = make(map[string]string) @@ -90,11 +132,14 @@ func GetENV() error { MonitorType = getEnvWithCheck("MonitorType", &missingEnvVars) clusterNS := getEnvWithCheck("ClusterNS", &missingEnvVars) LOCALREGION = getEnvWithCheck("LOCALREGION", &missingEnvVars) + GlobalCockroachURI = getEnvWithCheck("GlobalCockroachURI", &missingEnvVars) + LocalCockroachURI = getEnvWithCheck("LocalCockroachURI", &missingEnvVars) DatabaseMonitor, _ = strconv.ParseBool(getEnvWithCheck("DatabaseMonitor", &missingEnvVars)) DiskMonitor, _ = strconv.ParseBool(getEnvWithCheck("DiskMonitor", &missingEnvVars)) CPUMemMonitor, _ = strconv.ParseBool(getEnvWithCheck("CPUMemMonitor", &missingEnvVars)) BackupMonitor, _ = strconv.ParseBool(getEnvWithCheck("BackupMonitor", &missingEnvVars)) QuotaMonitor, _ = strconv.ParseBool(getEnvWithCheck("QuotaMonitor", &missingEnvVars)) + CockroachMonitor, _ = strconv.ParseBool(getEnvWithCheck("CockroachMonitor", &missingEnvVars)) DatabaseDiskMonitorThreshold, _ = strconv.ParseFloat(getEnvWithCheck("DatabaseDiskMonitorThreshold", &missingEnvVars), 64) DatabaseExceptionMonitorThreshold, _ = strconv.ParseFloat(getEnvWithCheck("DatabaseExceptionMonitorThreshold", &missingEnvVars), 64) DatabaseCPUMonitorThreshold, _ = strconv.ParseFloat(getEnvWithCheck("DatabaseCPUMonitorThreshold", &missingEnvVars), 64) @@ -119,6 +164,8 @@ func GetENV() error { "FeishuWebhookURLBackup", //Quota "FeishuWebhookURLQuota", + //CockroachDB + "FeishuWebhookURLCockroachDB", }, FeishuWebhookURLMap, &missingEnvVars) // Get ClusterRegionMap diff --git a/service/exceptionmonitor/dao/init.go b/service/exceptionmonitor/dao/init.go index e865536e75f..8215c6fb284 100644 --- a/service/exceptionmonitor/dao/init.go +++ b/service/exceptionmonitor/dao/init.go @@ -3,9 +3,8 @@ package dao import ( "os" - "github.com/labring/sealos/service/exceptionmonitor/api" - "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/service/exceptionmonitor/api" ) var ( diff --git a/service/exceptionmonitor/go.mod b/service/exceptionmonitor/go.mod index c3b3589e9e7..dcfc8c233d8 100644 --- a/service/exceptionmonitor/go.mod +++ b/service/exceptionmonitor/go.mod @@ -31,7 +31,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -41,6 +41,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/larksuite/oapi-sdk-go/v3 v3.2.9 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matoous/go-nanoid/v2 v2.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -51,7 +52,7 @@ require ( go.mongodb.org/mongo-driver v1.12.1 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect @@ -70,7 +71,7 @@ require ( k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) replace ( diff --git a/service/exceptionmonitor/go.sum b/service/exceptionmonitor/go.sum index 90fef7e8a9f..2aa954a8fde 100644 --- a/service/exceptionmonitor/go.sum +++ b/service/exceptionmonitor/go.sum @@ -99,6 +99,8 @@ github.com/larksuite/oapi-sdk-go v1.1.48 h1:RHRr5LW68AibBzXVRXObUpkbS6TXapl4TAyh github.com/larksuite/oapi-sdk-go v1.1.48/go.mod h1:7ybKAbVdKBjXuX0YrMTfnWUyCaIe/zeI1wqjNfN9XOk= github.com/larksuite/oapi-sdk-go/v3 v3.2.9 h1:9zQAGrzhibNwdaGRkWUP1cAd2k2dJJDpbSffcfK0wPw= github.com/larksuite/oapi-sdk-go/v3 v3.2.9/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= diff --git a/service/exceptionmonitor/helper/monitor/cockroachdb_monitor.go b/service/exceptionmonitor/helper/monitor/cockroachdb_monitor.go new file mode 100644 index 00000000000..c2a5ece3e6a --- /dev/null +++ b/service/exceptionmonitor/helper/monitor/cockroachdb_monitor.go @@ -0,0 +1,54 @@ +package monitor + +import ( + "fmt" + "log" + "time" + + "github.com/labring/sealos/service/exceptionmonitor/api" + "github.com/labring/sealos/service/exceptionmonitor/helper/notification" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func CockroachMonitor() { + for api.CockroachMonitor { + notificationInfo := &api.Info{ + FeishuWebHook: api.FeishuWebhookURLMap["FeishuWebhookURLCockroachDB"], + } + monitorCockroachDB(api.GlobalCockroachURI, "Global", notificationInfo) + monitorCockroachDB(api.LocalCockroachURI, "Local", notificationInfo) + + time.Sleep(5 * time.Minute) + } +} + +func monitorCockroachDB(uri, label string, notificationInfo *api.Info) { + if err := checkCockroachDB(uri); err != nil { + message := notification.GetCockroachMessage(err.Error(), label) + if sendErr := notification.SendFeishuNotification(notificationInfo, message); sendErr != nil { + log.Printf("Failed to send Feishu notification for %s: %v", label, sendErr) + } + } +} + +func checkCockroachDB(CockroachConnection string) error { + db, err := gorm.Open(postgres.Open(CockroachConnection), &gorm.Config{ + Logger: logger.Discard, + }) + if err != nil { + return fmt.Errorf("failed to connect to CockroachDB: %v", err) + } + + sqlDB, err := db.DB() + if err != nil { + return fmt.Errorf("failed to get database instance: %v", err) + } + defer sqlDB.Close() + + if err := sqlDB.Ping(); err != nil { + return fmt.Errorf("failed to ping CockroachDB: %v", err) + } + return nil +} diff --git a/service/exceptionmonitor/helper/monitor/database_backup_monitor.go b/service/exceptionmonitor/helper/monitor/database_backup_monitor.go index c690a34a685..d5cce4f993d 100644 --- a/service/exceptionmonitor/helper/monitor/database_backup_monitor.go +++ b/service/exceptionmonitor/helper/monitor/database_backup_monitor.go @@ -90,17 +90,18 @@ func processBackup(backup unstructured.Unstructured) { } func SendBackupNotification(backupName, namespace, status, startTimestamp string) { - notificationInfo := notification.Info{ + notificationInfo := api.Info{ DatabaseClusterName: backupName, Namespace: namespace, - Status: status, + ExceptionStatus: status, ExceptionType: "备份", PerformanceType: "Backup", - NotificationType: "exception", + NotificationType: notification.ExceptionType, + FeishuWebHook: api.FeishuWebhookURLMap["FeishuWebhookURLBackup"], } if _, ok := api.LastBackupStatusMap[backupName]; !ok { - message := notification.GetBackupMessage("exception", namespace, backupName, status, startTimestamp, "") - if err := notification.SendFeishuNotification(notificationInfo, message, api.FeishuWebhookURLMap["FeishuWebhookURLBackup"]); err != nil { + message := notification.GetBackupMessage(notification.ExceptionType, namespace, backupName, status, startTimestamp, "") + if err := notification.SendFeishuNotification(¬ificationInfo, message); err != nil { log.Printf("Error sending exception notification:%v", err) } api.LastBackupStatusMap[backupName] = status diff --git a/service/exceptionmonitor/helper/monitor/database_monitor.go b/service/exceptionmonitor/helper/monitor/database_monitor.go index 59eb8ad5949..eda713c0f4e 100644 --- a/service/exceptionmonitor/helper/monitor/database_monitor.go +++ b/service/exceptionmonitor/helper/monitor/database_monitor.go @@ -7,10 +7,9 @@ import ( "strings" "time" - "k8s.io/apimachinery/pkg/api/errors" - "github.com/labring/sealos/service/exceptionmonitor/api" "github.com/labring/sealos/service/exceptionmonitor/helper/notification" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -33,6 +32,14 @@ var ( Version: "v1", Resource: "users", } + databasePodNameMap = map[string]string{ + "redis": "redis", + "kafka": "kafka-broker", + "apecloud-mysql": "mysql", + "postgresql": "postgresql", + "milvus": "milvus", + "mongodb": "mongodb", + } ) func DatabaseExceptionMonitor() { @@ -46,11 +53,20 @@ func DatabaseExceptionMonitor() { } func checkDeletedDatabases() { - for databaseClusterUID, namespaceAndDatabaseClusterName := range api.DatabaseNamespaceMap { - namespace, databaseClusterName := getNamespaceAndDatabaseClusterName(namespaceAndDatabaseClusterName) - cluster, err := api.DynamicClient.Resource(databaseClusterGVR).Namespace(namespace).Get(context.Background(), databaseClusterName, metav1.GetOptions{}) + //for databaseClusterUID, namespaceAndDatabaseClusterName := range api.DatabaseNamespaceMap { + for _, notificationInfo := range api.DatabaseNotificationInfoMap { + //namespace, databaseClusterName := getNamespaceAndDatabaseClusterName(namespaceAndDatabaseClusterName) + cluster, err := api.DynamicClient.Resource(databaseClusterGVR).Namespace(notificationInfo.Namespace).Get(context.Background(), notificationInfo.DatabaseClusterName, metav1.GetOptions{}) if cluster == nil && errors.IsNotFound(err) { - handleClusterRecovery(databaseClusterUID, databaseClusterName, "", "Deleted") + //notificationInfo := api.Info{ + // DatabaseClusterUID: databaseClusterUID, + // Namespace: notificationInfo.Namespace, + // DatabaseClusterName: databaseClusterName, + // RecoveryStatus: "Deleted",ws + //} + notificationInfo.RecoveryStatus = "Deleted" + notificationInfo.RecoveryTime = time.Now().Format("2006-01-02 15:04:05") + handleClusterRecovery(notificationInfo) } } } @@ -88,99 +104,106 @@ func checkDatabasesInNamespace(namespace string) error { } func processCluster(cluster metav1unstructured.Unstructured) { - databaseClusterName, databaseType, namespace, databaseClusterUID := cluster.GetName(), cluster.GetLabels()[api.DatabaseTypeLabel], cluster.GetNamespace(), string(cluster.GetUID()) - status, _, err := metav1unstructured.NestedString(cluster.Object, "status", "phase") - if err != nil { - log.Printf("Unable to get %s status in ns %s: %v", databaseClusterName, namespace, err) - } - switch status { + // todo 获取数据库信息抽成一个函数,封装在notificationInfo中 + notificationInfo := api.Info{} + getClusterDatabaseInfo(cluster, ¬ificationInfo) + switch notificationInfo.ExceptionStatus { case api.StatusRunning, api.StatusStopped: - handleClusterRecovery(databaseClusterUID, databaseClusterName, namespace, status) + if value, ok := api.DatabaseNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok { + recoveryNotificationInfo := value + recoveryNotificationInfo.RecoveryStatus, recoveryNotificationInfo.RecoveryTime = getClusterDatabaseStatus(cluster, recoveryNotificationInfo) + handleClusterRecovery(recoveryNotificationInfo) + } case api.StatusDeleting, api.StatusStopping: - // No action needed + // nothing to do break case api.StatusUnknown: - if _, ok := api.LastDatabaseClusterStatus[databaseClusterUID]; !ok { - api.LastDatabaseClusterStatus[databaseClusterUID] = status - api.DatabaseNamespaceMap[databaseClusterUID] = namespace + "-" + databaseClusterName - api.ExceptionDatabaseMap[databaseClusterUID] = true - alertMessage, feishuWebHook, notification := prepareAlertMessage(databaseClusterUID, databaseClusterName, namespace, status, "", "status is empty", 0) - if err := sendAlert(alertMessage, feishuWebHook, databaseClusterUID, notification); err != nil { - log.Printf("Failed to send feishu %s in ns %s: %v", databaseClusterName, namespace, err) + if _, ok := api.DatabaseNotificationInfoMap[notificationInfo.DatabaseClusterUID]; !ok { + api.DatabaseNotificationInfoMap[notificationInfo.DatabaseClusterUID] = ¬ificationInfo + //api.LastDatabaseClusterStatus[notificationInfo.DatabaseClusterUID] = notificationInfo.ExceptionStatus + //api.DatabaseNamespaceMap[notificationInfo.DatabaseClusterUID] = notificationInfo.Namespace + "-" + notificationInfo.DatabaseClusterName + //api.ExceptionDatabaseMap[notificationInfo.DatabaseClusterUID] = true + notificationInfo.Events = "status is empty" + notificationInfo.DebtLevel = "" + alertMessage := prepareAlertMessage(¬ificationInfo, 0) + if err := sendAlert(alertMessage, ¬ificationInfo); err != nil { + log.Printf("Failed to send feishu %s in ns %s: %v", notificationInfo.DatabaseClusterName, notificationInfo.Namespace, err) } } default: - handleClusterException(databaseClusterUID, databaseClusterName, namespace, databaseType, status) + //Updating、Creating、Failed、Abnormal + //notificationInfo.DatabaseClusterUID, databaseClusterName, namespace, databaseType, status + handleClusterException(¬ificationInfo) } } -func handleClusterRecovery(databaseClusterUID, databaseClusterName, namespace, status string) { - if api.ExceptionDatabaseMap[databaseClusterUID] { - notificationInfo := notification.Info{ - DatabaseClusterName: databaseClusterName, - Namespace: namespace, - Status: status, - ExceptionType: "状态", - NotificationType: "recovery", - } - recoveryMessage := notification.GetNotificationMessage(notificationInfo) - if err := notification.SendFeishuNotification(notificationInfo, recoveryMessage, api.FeishuWebHookMap[databaseClusterUID]); err != nil { - log.Printf("Error sending recovery notification: %v", err) - } - cleanClusterStatus(databaseClusterUID) +func handleClusterRecovery(notificationInfo *api.Info) { + //if api.ExceptionDatabaseMap[notificationInfo.DatabaseClusterUID] { + notificationInfo.NotificationType = "recovery" + recoveryMessage := notification.GetNotificationMessage(notificationInfo) + //getClusterDatabaseStatus应该在上层去做,因为有可能是已经删的数据状态更新信息,在这里获取的话,就没法拿到状态信息 + //notificationInfo.RecoveryStatus, notificationInfo.RecoveryStatusTime = getClusterDatabaseStatus(cluster, notificationInfo) + if err := notification.SendFeishuNotification(notificationInfo, recoveryMessage); err != nil { + log.Printf("Error sending recovery notification: %v", err) } + cleanClusterStatus(notificationInfo.DatabaseClusterUID) } func cleanClusterStatus(databaseClusterUID string) { - delete(api.LastDatabaseClusterStatus, databaseClusterUID) + delete(api.DatabaseNotificationInfoMap, databaseClusterUID) delete(api.DiskFullNamespaceMap, databaseClusterUID) - delete(api.FeishuWebHookMap, databaseClusterUID) - delete(api.ExceptionDatabaseMap, databaseClusterUID) - delete(api.DatabaseNamespaceMap, databaseClusterUID) + //delete(api.FeishuWebHookMap, databaseClusterUID) + //delete(api.ExceptionDatabaseMap, databaseClusterUID) + //delete(api.DatabaseNamespaceMap, databaseClusterUID) } -func handleClusterException(databaseClusterUID, databaseClusterName, namespace, databaseType, status string) { - if _, ok := api.LastDatabaseClusterStatus[databaseClusterUID]; !ok && !api.DebtNamespaceMap[namespace] { - api.LastDatabaseClusterStatus[databaseClusterUID] = status - api.DatabaseNamespaceMap[databaseClusterUID] = namespace + "-" + databaseClusterName - api.ExceptionDatabaseMap[databaseClusterUID] = true - if err := processClusterException(databaseClusterUID, databaseClusterName, namespace, databaseType, status); err != nil { - log.Printf("Failed to process cluster %s exception in ns %s: %v", databaseClusterName, namespace, err) +func handleClusterException(notificationInfo *api.Info) { + if _, ok := api.DatabaseNotificationInfoMap[notificationInfo.DatabaseClusterUID]; !ok && !api.DebtNamespaceMap[notificationInfo.Namespace] { + api.DatabaseNotificationInfoMap[notificationInfo.DatabaseClusterUID] = notificationInfo + //api.LastDatabaseClusterStatus[notificationInfo.DatabaseClusterUID] = notificationInfo.ExceptionStatus + //api.DatabaseNamespaceMap[notificationInfo.DatabaseClusterUID] = notificationInfo.Namespace + "-" + notificationInfo.DatabaseClusterName + //api.ExceptionDatabaseMap[notificationInfo.DatabaseClusterUID] = true + //notificationInfo.DatabaseClusterUID, databaseClusterName, namespace, databaseType, status + if err := processClusterException(notificationInfo); err != nil { + log.Printf("Failed to process cluster %s exception in ns %s: %v", notificationInfo.DatabaseClusterName, notificationInfo.Namespace, err) } } - //if !api.DebtNamespaceMap[namespace] && !api.ExceptionDatabaseMap[databaseClusterName] { - // - //} } -func processClusterException(databaseClusterUID, databaseClusterName, namespace, databaseType, status string) error { - debt, debtLevel, _ := checkDebt(namespace) +func processClusterException(notificationInfo *api.Info) error { + debt, debtLevel, _ := checkDebt(notificationInfo.Namespace) + notificationInfo.DebtLevel = debtLevel if debt { - databaseEvents, send := getDatabaseClusterEvents(databaseClusterName, namespace) + //databaseClusterName, namespace + databaseEvents, send := getDatabaseClusterEvents(notificationInfo) if send { - maxUsage, err := checkPerformance(namespace, databaseClusterName, databaseType, "disk") + //namespace, databaseClusterName, databaseType + maxUsage, err := checkPerformance(notificationInfo, "disk") if err != nil { return err } - alertMessage, feishuWebHook, notification := prepareAlertMessage(databaseClusterUID, databaseClusterName, namespace, status, debtLevel, databaseEvents, maxUsage) - if err := sendAlert(alertMessage, feishuWebHook, databaseClusterUID, notification); err != nil { + notificationInfo.Events = databaseEvents + //notificationInfo.DatabaseClusterUID, databaseClusterName, namespace, status, debtLevel, databaseEvents + alertMessage := prepareAlertMessage(notificationInfo, maxUsage) + if err := sendAlert(alertMessage, notificationInfo); err != nil { return err } } else { - if err := notifyQuotaExceeded(databaseClusterName, namespace, status, debtLevel); err != nil { + //databaseClusterName, namespace, status, debtLevel + if err := notifyQuotaExceeded(notificationInfo); err != nil { return err } } } else { - api.DebtNamespaceMap[namespace] = true - delete(api.LastDatabaseClusterStatus, databaseClusterUID) + api.DebtNamespaceMap[notificationInfo.Namespace] = true + delete(api.DatabaseNotificationInfoMap, notificationInfo.DatabaseClusterUID) } return nil } -func getDatabaseClusterEvents(databaseClusterName, namespace string) (string, bool) { - events, err := api.ClientSet.CoreV1().Events(namespace).List(context.TODO(), metav1.ListOptions{ - FieldSelector: fmt.Sprintf("involvedObject.name=%s", databaseClusterName), +func getDatabaseClusterEvents(notificationInfo *api.Info) (string, bool) { + events, err := api.ClientSet.CoreV1().Events(notificationInfo.Namespace).List(context.TODO(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("involvedObject.name=%s", notificationInfo.DatabaseClusterName), }) if err != nil { fmt.Printf("Failed get events from databaseCluster: %v\n", err) @@ -198,61 +221,66 @@ func databaseQuotaExceptionFilter(databaseEvents string) bool { return !strings.Contains(databaseEvents, api.ExceededQuotaException) } -func prepareAlertMessage(databaseClusterUID, databaseClusterName, namespace, status, debtLevel, databaseEvents string, maxUsage float64) (string, string, notification.Info) { - alertMessage, feishuWebHook := "", "" - notificationInfo := notification.Info{ - DatabaseClusterName: databaseClusterName, - Namespace: namespace, - Status: status, - DebtLevel: debtLevel, - ExceptionType: "状态", - Events: databaseEvents, - NotificationType: "exception", - } +func prepareAlertMessage(notificationInfo *api.Info, maxUsage float64) string { + alertMessage := "" + notificationInfo.ExceptionType = "状态" + notificationInfo.NotificationType = notification.ExceptionType if maxUsage < api.DatabaseExceptionMonitorThreshold { //status == "Creating" || status == "Deleting" || status == "Stopping" - if status == "Creating" { - feishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLCSD"] + if notificationInfo.ExceptionStatus == "Creating" { + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLCSD"] } else { - feishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLUFA"] + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLUFA"] } alertMessage = notification.GetNotificationMessage(notificationInfo) } else { - if !api.DiskFullNamespaceMap[databaseClusterUID] { - feishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLOther"] + if !api.DiskFullNamespaceMap[notificationInfo.DatabaseClusterUID] { + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLOther"] notificationInfo.Reason = "disk is full" alertMessage = notification.GetNotificationMessage(notificationInfo) - notification.CreateNotification(namespace, databaseClusterName, status, "disk is full", "磁盘满了") + //namespace, databaseClusterName, status + notification.CreateNotification(notificationInfo, "disk is full", "磁盘满了") } - api.DiskFullNamespaceMap[databaseClusterUID] = true + api.DiskFullNamespaceMap[notificationInfo.DatabaseClusterUID] = true } - return alertMessage, feishuWebHook, notificationInfo + return alertMessage } -func sendAlert(alertMessage, feishuWebHook, databaseClusterUID string, notificationInfo notification.Info) error { - api.FeishuWebHookMap[databaseClusterUID] = feishuWebHook - return notification.SendFeishuNotification(notificationInfo, alertMessage, feishuWebHook) +func sendAlert(alertMessage string, notificationInfo *api.Info) error { + //api.FeishuWebHookMap[notificationInfo.DatabaseClusterUID] = feishuWebHook + return notification.SendFeishuNotification(notificationInfo, alertMessage) } -func notifyQuotaExceeded(databaseClusterName, namespace, status, debtLevel string) error { - notificationInfo := notification.Info{ - DatabaseClusterName: databaseClusterName, - Namespace: namespace, - Status: status, - DebtLevel: debtLevel, - Reason: api.ExceededQuotaException, - ExceptionType: "状态", - NotificationType: "exception", - } +func notifyQuotaExceeded(notificationInfo *api.Info) error { + notificationInfo.ExceptionType = "状态" + notificationInfo.Reason = api.ExceededQuotaException + notificationInfo.NotificationType = notification.ExceptionType + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLOther"] alertMessage := notification.GetNotificationMessage(notificationInfo) - notification.CreateNotification(namespace, databaseClusterName, status, api.ExceededQuotaException, "Quato满了") - return notification.SendFeishuNotification(notificationInfo, alertMessage, api.FeishuWebhookURLMap["FeishuWebhookURLOther"]) + notification.CreateNotification(notificationInfo, api.ExceededQuotaException, "Quato满了") + return notification.SendFeishuNotification(notificationInfo, alertMessage) +} + +func getClusterDatabaseInfo(cluster metav1unstructured.Unstructured, notificationInfo *api.Info) { + databaseClusterName, databaseType, namespace, databaseClusterUID := cluster.GetName(), cluster.GetLabels()[api.DatabaseTypeLabel], cluster.GetNamespace(), string(cluster.GetUID()) + notificationInfo.DatabaseType = databaseType + notificationInfo.Namespace = namespace + notificationInfo.DatabaseClusterName = databaseClusterName + notificationInfo.DatabaseClusterUID = databaseClusterUID + notificationInfo.ExceptionStatus, notificationInfo.ExceptionStatusTime = getClusterDatabaseStatus(cluster, notificationInfo) } -func getNamespaceAndDatabaseClusterName(namespaceAndDatabaseClusterName string) (string, string) { - firstIndex := strings.Index(namespaceAndDatabaseClusterName, "-") - secondIndex := strings.Index(namespaceAndDatabaseClusterName[firstIndex+1:], "-") + firstIndex + 1 - namespace := namespaceAndDatabaseClusterName[:secondIndex] - databaseClusterName := namespaceAndDatabaseClusterName[secondIndex+1:] - return namespace, databaseClusterName +func getClusterDatabaseStatus(cluster metav1unstructured.Unstructured, notificationInfo *api.Info) (string, string) { + status, _, _ := metav1unstructured.NestedString(cluster.Object, "status", "phase") + + databaseClusterStatus, _, _ := metav1unstructured.NestedMap(cluster.Object, "status") + + podName := databasePodNameMap[notificationInfo.DatabaseType] + podsReadyTime, _, _ := metav1unstructured.NestedString(databaseClusterStatus, "components", podName, "podsReadyTime") + + parsedTime, _ := time.Parse(time.RFC3339, podsReadyTime) + adjustedTime := parsedTime.Add(8 * time.Hour) + + formattedTime := adjustedTime.Format("2006-01-02 15:04:05") + return status, formattedTime } diff --git a/service/exceptionmonitor/helper/monitor/database_performance_monitor.go b/service/exceptionmonitor/helper/monitor/database_performance_monitor.go index 25523538d75..de6708a2ab5 100644 --- a/service/exceptionmonitor/helper/monitor/database_performance_monitor.go +++ b/service/exceptionmonitor/helper/monitor/database_performance_monitor.go @@ -56,91 +56,133 @@ func checkDatabasePerformanceInNamespace(namespace string) error { } func monitorCluster(cluster unstructured.Unstructured) { - databaseClusterName, databaseType, namespace, UID := cluster.GetName(), cluster.GetLabels()[api.DatabaseTypeLabel], cluster.GetNamespace(), string(cluster.GetUID()) - status, found, err := unstructured.NestedString(cluster.Object, "status", "phase") - if err != nil || !found { - log.Printf("Unable to get %s status in ns %s: %v", databaseClusterName, namespace, err) - } - debt, _, _ := checkDebt(namespace) + notificationInfo := api.Info{} + getClusterDatabaseInfo(cluster, ¬ificationInfo) + debt, _, _ := checkDebt(notificationInfo.Namespace) if !debt { return } - info := notification.Info{ - DatabaseClusterName: databaseClusterName, - Namespace: namespace, - Status: status, - NotificationType: "exception", - ExceptionType: "阀值", + notificationInfo.ExceptionType = "阀值" + if value, ok := api.CPUNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok { + notificationInfo = *value + } else if value, ok := api.MemNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok { + notificationInfo = *value + } else if value, ok := api.DiskNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok { + notificationInfo = *value } - switch status { + switch notificationInfo.ExceptionStatus { case api.StatusDeleting, api.StatusCreating, api.StatusStopping, api.StatusStopped, api.StatusUnknown: break default: if api.CPUMemMonitor { - handleCPUMemMonitor(namespace, databaseClusterName, databaseType, UID, info) + handleCPUMemMonitor(¬ificationInfo) } if api.DiskMonitor { - handleDiskMonitor(namespace, databaseClusterName, databaseType, UID, info) + handleDiskMonitor(¬ificationInfo) } } } -func handleCPUMemMonitor(namespace, databaseClusterName, databaseType, UID string, info notification.Info) { - if cpuUsage, err := CPUMemMonitor(namespace, databaseClusterName, databaseType, "cpu"); err == nil { - processUsage(cpuUsage, api.DatabaseCPUMonitorThreshold, "CPU", UID, info, api.CPUMonitorNamespaceMap) +func handleCPUMemMonitor(notificationInfo *api.Info) { + if cpuUsage, err := CPUMemMonitor(notificationInfo, "cpu"); err == nil { + processUsage(cpuUsage, api.DatabaseCPUMonitorThreshold, api.CPUChinese, notificationInfo) } else { log.Printf("Failed to monitor CPU: %v", err) } - if memUsage, err := CPUMemMonitor(namespace, databaseClusterName, databaseType, "memory"); err == nil { - processUsage(memUsage, api.DatabaseMemMonitorThreshold, "内存", UID, info, api.MemMonitorNamespaceMap) + if memUsage, err := CPUMemMonitor(notificationInfo, "memory"); err == nil { + processUsage(memUsage, api.DatabaseMemMonitorThreshold, api.MemoryChinese, notificationInfo) } else { log.Printf("Failed to monitor Memory: %v", err) } } -func handleDiskMonitor(namespace, databaseClusterName, databaseType, UID string, info notification.Info) { - if maxUsage, err := checkPerformance(namespace, databaseClusterName, databaseType, "disk"); err == nil { - processUsage(maxUsage, api.DatabaseDiskMonitorThreshold, "磁盘", UID, info, api.DiskMonitorNamespaceMap) +func handleDiskMonitor(notificationInfo *api.Info) { + if maxUsage, err := checkPerformance(notificationInfo, "disk"); err == nil { + processUsage(maxUsage, api.DatabaseDiskMonitorThreshold, api.DiskChinese, notificationInfo) } else { log.Printf("Failed to monitor Disk: %v", err) } } -func processUsage(usage float64, threshold float64, performanceType, UID string, info notification.Info, monitorMap map[string]bool) { - info.PerformanceType = performanceType +func processUsage(usage float64, threshold float64, performanceType string, notificationInfo *api.Info) { + notificationInfo.PerformanceType = performanceType usageStr := strconv.FormatFloat(usage, 'f', 2, 64) - if performanceType == "CPU" { - info.CPUUsage = usageStr - } else if performanceType == "内存" { - info.MemUsage = usageStr - } else if performanceType == "磁盘" { - info.DiskUsage = usageStr - } - if usage >= threshold && !monitorMap[UID] { - alertMessage := notification.GetNotificationMessage(info) - if err := notification.SendFeishuNotification(info, alertMessage, api.FeishuWebhookURLMap["FeishuWebhookURLImportant"]); err != nil { - log.Printf("Failed to send notification: %v", err) + if notificationInfo.PerformanceType == api.CPUChinese { + notificationInfo.CPUUsage = usageStr + } else if performanceType == api.MemoryChinese { + notificationInfo.MemUsage = usageStr + } else if performanceType == api.DiskChinese { + notificationInfo.DiskUsage = usageStr + } + if usage >= threshold { + if _, ok := api.CPUNotificationInfoMap[notificationInfo.DatabaseClusterUID]; !ok && notificationInfo.PerformanceType == api.CPUChinese { + processException(notificationInfo, threshold) + } + if _, ok := api.MemNotificationInfoMap[notificationInfo.DatabaseClusterUID]; !ok && notificationInfo.PerformanceType == api.MemoryChinese { + processException(notificationInfo, threshold) } - monitorMap[UID] = true - if performanceType != "磁盘" { - return + if _, ok := api.DiskNotificationInfoMap[notificationInfo.DatabaseClusterUID]; !ok && notificationInfo.PerformanceType == api.DiskChinese { + processException(notificationInfo, threshold) } - ZNThreshold := NumberToChinese(int(threshold)) - if err := notification.SendToSms(info.Namespace, info.DatabaseClusterName, api.ClusterName, "数据库"+performanceType+"超过百分之"+ZNThreshold); err != nil { - log.Printf("Failed to send Sms: %v", err) + } else if usage < threshold { + if _, ok := api.CPUNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok && notificationInfo.PerformanceType == api.CPUChinese { + processRecovery(notificationInfo) } - } else if usage < threshold && monitorMap[UID] { - info.NotificationType = "recovery" - alertMessage := notification.GetNotificationMessage(info) - if err := notification.SendFeishuNotification(info, alertMessage, api.FeishuWebhookURLMap["FeishuWebhookURLImportant"]); err != nil { - log.Printf("Failed to send notification: %v", err) + if _, ok := api.MemNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok && notificationInfo.PerformanceType == api.MemoryChinese { + processRecovery(notificationInfo) } - delete(monitorMap, UID) + if _, ok := api.DiskNotificationInfoMap[notificationInfo.DatabaseClusterUID]; ok && notificationInfo.PerformanceType == api.DiskChinese { + processRecovery(notificationInfo) + } + } +} + +func processException(notificationInfo *api.Info, threshold float64) { + notificationInfo.NotificationType = notification.ExceptionType + alertMessage := notification.GetNotificationMessage(notificationInfo) + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLImportant"] + if err := notification.SendFeishuNotification(notificationInfo, alertMessage); err != nil { + log.Printf("Failed to send notification: %v", err) + } + if notificationInfo.PerformanceType == api.CPUChinese { + api.CPUNotificationInfoMap[notificationInfo.DatabaseClusterUID] = notificationInfo + return + } + if notificationInfo.PerformanceType == api.MemoryChinese { + api.MemNotificationInfoMap[notificationInfo.DatabaseClusterUID] = notificationInfo + return + } + if notificationInfo.PerformanceType == api.DiskChinese { + api.DiskNotificationInfoMap[notificationInfo.DatabaseClusterUID] = notificationInfo + } + ZNThreshold := NumberToChinese(int(threshold)) + if err := notification.SendToSms(notificationInfo, api.ClusterName, "数据库"+notificationInfo.PerformanceType+"超过百分之"+ZNThreshold); err != nil { + log.Printf("Failed to send Sms: %v", err) + } +} + +func processRecovery(notificationInfo *api.Info) { + notificationInfo.NotificationType = "recovery" + notificationInfo.RecoveryStatus = notificationInfo.ExceptionStatus + notificationInfo.RecoveryTime = time.Now().Add(8 * time.Hour).Format("2006-01-02 15:04:05") + alertMessage := notification.GetNotificationMessage(notificationInfo) + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLImportant"] + if err := notification.SendFeishuNotification(notificationInfo, alertMessage); err != nil { + log.Printf("Failed to send notification: %v", err) + } + if notificationInfo.PerformanceType == api.CPUChinese { + delete(api.CPUNotificationInfoMap, notificationInfo.DatabaseClusterUID) + } + if notificationInfo.PerformanceType == api.MemoryChinese { + delete(api.MemNotificationInfoMap, notificationInfo.DatabaseClusterUID) + } + if notificationInfo.PerformanceType == api.DiskChinese { + delete(api.DiskNotificationInfoMap, notificationInfo.DatabaseClusterUID) } } -func CPUMemMonitor(namespace, databaseClusterName, databaseType, checkType string) (float64, error) { - return checkPerformance(namespace, databaseClusterName, databaseType, checkType) +func CPUMemMonitor(notificationInfo *api.Info, checkType string) (float64, error) { + return checkPerformance(notificationInfo, checkType) } func NumberToChinese(num int) string { diff --git a/service/exceptionmonitor/helper/monitor/performance.go b/service/exceptionmonitor/helper/monitor/performance.go index db3b993da71..d7cff33dac1 100644 --- a/service/exceptionmonitor/helper/monitor/performance.go +++ b/service/exceptionmonitor/helper/monitor/performance.go @@ -15,11 +15,11 @@ import ( "github.com/labring/sealos/service/exceptionmonitor/api" ) -func checkPerformance(namespace, databaseClusterName, databaseType, checkType string) (float64, error) { +func checkPerformance(notificationInfo *api.Info, checkType string) (float64, error) { params := url.Values{} - params.Add("namespace", namespace) - params.Add("app", databaseClusterName) - params.Add("type", databaseType) + params.Add("namespace", notificationInfo.Namespace) + params.Add("app", notificationInfo.DatabaseClusterName) + params.Add("type", notificationInfo.DatabaseType) params.Add("query", checkType) urlStr := api.BaseURL + "?" + params.Encode() @@ -29,7 +29,7 @@ func checkPerformance(namespace, databaseClusterName, databaseType, checkType st return 0.0, err } - kubeconfig, err := getKubeConfig(namespace) + kubeconfig, err := getKubeConfig(notificationInfo.Namespace) if err != nil { return 0.0, err } diff --git a/service/exceptionmonitor/helper/monitor/quota_monitor.go b/service/exceptionmonitor/helper/monitor/quota_monitor.go index 87a201381ff..26d6a6b242f 100644 --- a/service/exceptionmonitor/helper/monitor/quota_monitor.go +++ b/service/exceptionmonitor/helper/monitor/quota_monitor.go @@ -16,18 +16,27 @@ import ( func QuotaMonitor() { for api.QuotaMonitor { - if err := checkQuota(); err != nil { + if err := checkQuota(api.ClusterNS); err != nil { log.Printf("Failed to check qouta: %v", err) } time.Sleep(3 * time.Hour) } } -func checkQuota() error { - namespaceList, _ := api.ClientSet.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) +func checkQuota(namespaces []string) error { + var namespaceList []v1.Namespace - fmt.Println(len(namespaceList.Items)) - for _, ns := range namespaceList.Items { + // Fetch namespaces based on MonitorType + if api.MonitorType == api.MonitorTypeALL { + namespaces, _ := api.ClientSet.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + namespaceList = namespaces.Items + } else { + for _, ns := range namespaces { + namespace, _ := api.ClientSet.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}) + namespaceList = append(namespaceList, *namespace) + } + } + for _, ns := range namespaceList { if !strings.Contains(ns.Name, "ns-") { continue } @@ -39,18 +48,19 @@ func checkQuota() error { if len(quotaList.Items) != 1 || quotaList.Items[0].Name != "quota-"+ns.Name { continue } - nsQuota := notification.NameSpaceQuota{ + nsQuota := api.NameSpaceQuota{ NameSpace: ns.Name, } - notificationInfo := notification.Info{ + notificationInfo := api.Info{ ExceptionType: "Quota", PerformanceType: "Quota", - NotificationType: "exception", + NotificationType: notification.ExceptionType, } send := processQuota(quotaList, &nsQuota) if send { message := notification.GetQuotaMessage(&nsQuota) - if err := notification.SendFeishuNotification(notificationInfo, message, api.FeishuWebhookURLMap["FeishuWebhookURLQuota"]); err != nil { + notificationInfo.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLQuota"] + if err := notification.SendFeishuNotification(¬ificationInfo, message); err != nil { log.Printf("Error sending exception notification:%v", err) } } @@ -58,7 +68,7 @@ func checkQuota() error { return nil } -func processQuota(quotaList *v1.ResourceQuotaList, nsQuota *notification.NameSpaceQuota) bool { +func processQuota(quotaList *v1.ResourceQuotaList, nsQuota *api.NameSpaceQuota) bool { send := false for resourceName, hardQuantity := range quotaList.Items[0].Status.Hard { usedQuantity, exists := quotaList.Items[0].Status.Used[resourceName] diff --git a/service/exceptionmonitor/helper/notification/desktop.go b/service/exceptionmonitor/helper/notification/desktop.go index bc2c626c4d9..5d3908b1c11 100644 --- a/service/exceptionmonitor/helper/notification/desktop.go +++ b/service/exceptionmonitor/helper/notification/desktop.go @@ -31,7 +31,7 @@ func randString(n int) (string, error) { return string(b), nil } -func CreateNotification(namespace, name, status, notificationMessage, zhNotificationMessage string) { +func CreateNotification(notificationInfo *api.Info, notificationMessage, zhNotificationMessage string) { gvr := schema.GroupVersionResource{ Group: "notification.sealos.io", Version: "v1", @@ -40,14 +40,14 @@ func CreateNotification(namespace, name, status, notificationMessage, zhNotifica randomSuffix, _ := randString(5) now := time.Now().UTC().Unix() - message := fmt.Sprintf("Because %s , Database %s current status : %s , Please check in time.", notificationMessage, name, status) - zhMessage := fmt.Sprintf("因为 %s , 数据库 %s 当前状态 : %s , 请及时检查.", zhNotificationMessage, name, status) + message := fmt.Sprintf("Because %s , Database %s current status : %s , Please check in time.", notificationMessage, notificationInfo.DatabaseClusterName, notificationInfo.ExceptionStatus) + zhMessage := fmt.Sprintf("因为 %s , 数据库 %s 当前状态 : %s , 请及时检查.", zhNotificationMessage, notificationInfo.DatabaseClusterName, notificationInfo.ExceptionStatus) notification := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "notification.sealos.io/v1", "kind": "Notification", "metadata": map[string]interface{}{ - "name": "database-exception" + name + randomSuffix, + "name": "database-exception" + notificationInfo.DatabaseClusterName + randomSuffix, }, "spec": map[string]interface{}{ "title": "Database Exception", @@ -67,7 +67,7 @@ func CreateNotification(namespace, name, status, notificationMessage, zhNotifica }, } - _, err := api.DynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), notification, metav1.CreateOptions{}) + _, err := api.DynamicClient.Resource(gvr).Namespace(notificationInfo.Namespace).Create(context.TODO(), notification, metav1.CreateOptions{}) if err != nil { log.Printf("Failed to send desktop notification: %v", err) } diff --git a/service/exceptionmonitor/helper/notification/feishu.go b/service/exceptionmonitor/helper/notification/feishu.go index 312547f9866..376b678a19f 100644 --- a/service/exceptionmonitor/helper/notification/feishu.go +++ b/service/exceptionmonitor/helper/notification/feishu.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "regexp" - "time" "github.com/labring/sealos/service/exceptionmonitor/api" lark "github.com/larksuite/oapi-sdk-go/v3" @@ -20,55 +19,69 @@ var ( feiShuClient *lark.Client ) -type Info struct { - DatabaseClusterName string - Namespace string - Status string - DebtLevel string - Events string - Reason string - NotificationType string - DiskUsage string - CPUUsage string - MemUsage string - PerformanceType string - ExceptionType string +func InitFeishuClient() { + feiShuClient = lark.NewClient(api.APPID, api.APPSECRET) } -type NameSpaceQuota struct { - NameSpace string - CPULimit string - MemLimit string - GPULimit string - EphemeralStorageLimit string - ObjectStorageLimit string - NodePortLimit string - StorageLimit string - CPUUsage string - MemUsage string - GPUUsage string - EphemeralStorageUsage string - ObjectStorageUsage string - NodePortUsage string - StorageUsage string -} +func GetCockroachMessage(errMessage, cockroachType string) string { + headerTemplate := "red" + titleContent := "小强数据库异常告警" + elements := []map[string]interface{}{ + { + "tag": "div", + "text": map[string]string{ + "content": fmt.Sprintf("集群环境:%s", api.ClusterName), + "tag": "lark_md", + }, + }, + { + "tag": "div", + "text": map[string]string{ + "content": fmt.Sprintf("数据库类型:%s", cockroachType), + "tag": "lark_md", + }, + }, + { + "tag": "div", + "text": map[string]string{ + "content": fmt.Sprintf("异常信息:%s", errMessage), + "tag": "lark_md", + }, + }, + } + card := map[string]interface{}{ + "config": map[string]bool{ + "wide_screen_mode": true, + }, + "elements": elements, + "header": map[string]interface{}{ + "template": headerTemplate, + "title": map[string]string{ + "content": titleContent, + "tag": "plain_text", + }, + }, + } -func InitFeishuClient() { - feiShuClient = lark.NewClient(api.APPID, api.APPSECRET) + databaseMessage, err := json.Marshal(card) + if err != nil { + fmt.Println("Error marshaling JSON:", err) + return "" + } + return string(databaseMessage) } -func GetNotificationMessage(notificationInfo Info) string { +func GetNotificationMessage(notificationInfo *api.Info) string { headerTemplate := "red" titleContent := "数据库" + notificationInfo.ExceptionType + "告警" usage := "" - if notificationInfo.PerformanceType == "CPU" { + if notificationInfo.PerformanceType == api.CPUChinese { usage = notificationInfo.CPUUsage - } else if notificationInfo.PerformanceType == "内存" { + } else if notificationInfo.PerformanceType == api.MemoryChinese { usage = notificationInfo.MemUsage - } else if notificationInfo.PerformanceType == "磁盘" { + } else if notificationInfo.PerformanceType == api.DiskChinese { usage = notificationInfo.DiskUsage } - var elements []map[string]interface{} commonElements := []map[string]interface{}{ { @@ -95,7 +108,7 @@ func GetNotificationMessage(notificationInfo Info) string { { "tag": "div", "text": map[string]string{ - "content": fmt.Sprintf("数据库状态:%s", notificationInfo.Status), + "content": fmt.Sprintf("数据库状态:%s", notificationInfo.ExceptionStatus), "tag": "lark_md", }, }, @@ -104,6 +117,12 @@ func GetNotificationMessage(notificationInfo Info) string { if notificationInfo.NotificationType == ExceptionType && notificationInfo.ExceptionType == "状态" { exceptionElements := []map[string]interface{}{ { + "tag": "div", + "text": map[string]string{ + "content": fmt.Sprintf("数据库异常时间:%s", notificationInfo.ExceptionStatusTime), + "tag": "lark_md", + }, + }, { "tag": "div", "text": map[string]string{ "content": fmt.Sprintf("欠费级别:%s", notificationInfo.DebtLevel), @@ -125,8 +144,8 @@ func GetNotificationMessage(notificationInfo Info) string { }, }, } - elements = append(commonElements, exceptionElements...) - } else if notificationInfo.ExceptionType == "阀值" { + notificationInfo.FeishuInfo = append(commonElements, exceptionElements...) + } else if notificationInfo.NotificationType == ExceptionType && notificationInfo.ExceptionType == "阀值" { exceptionElements := []map[string]interface{}{ { "tag": "div", @@ -136,16 +155,26 @@ func GetNotificationMessage(notificationInfo Info) string { }, }, } - elements = append(commonElements, exceptionElements...) + notificationInfo.FeishuInfo = append(commonElements, exceptionElements...) } if notificationInfo.NotificationType == "recovery" { headerTemplate = "blue" titleContent = "数据库" + notificationInfo.ExceptionType + "恢复通知" - elements = commonElements + separatorElements := []map[string]interface{}{ + { + "tag": "div", + "text": map[string]string{ + "content": "-------------------------------------数据库恢复信息-------------------------------------", + "tag": "lark_md", + }, + }, + } + notificationInfo.FeishuInfo = append(notificationInfo.FeishuInfo, separatorElements...) + if notificationInfo.ExceptionType == "阀值" { - exceptionElements := []map[string]interface{}{ + usageRecoveryElements := []map[string]interface{}{ { "tag": "div", "text": map[string]string{ @@ -154,24 +183,31 @@ func GetNotificationMessage(notificationInfo Info) string { }, }, } - elements = append(elements, exceptionElements...) + notificationInfo.FeishuInfo = append(notificationInfo.FeishuInfo, usageRecoveryElements...) } - exceptionElements := []map[string]interface{}{ + recoveryTimeElements := []map[string]interface{}{ { "tag": "div", "text": map[string]string{ - "content": fmt.Sprintf("数据库恢复时间:%s", time.Now().Add(8*time.Hour).Format("2006-01-02 15:04:05")), + "content": fmt.Sprintf("数据库状态:%s", notificationInfo.RecoveryStatus), + "tag": "lark_md", + }, + }, + { + "tag": "div", + "text": map[string]string{ + "content": fmt.Sprintf("数据库恢复时间:%s", notificationInfo.RecoveryTime), "tag": "lark_md", }, }, } - elements = append(elements, exceptionElements...) + notificationInfo.FeishuInfo = append(notificationInfo.FeishuInfo, recoveryTimeElements...) } card := map[string]interface{}{ "config": map[string]bool{ "wide_screen_mode": true, }, - "elements": elements, + "elements": notificationInfo.FeishuInfo, "header": map[string]interface{}{ "template": headerTemplate, "title": map[string]string{ @@ -189,9 +225,9 @@ func GetNotificationMessage(notificationInfo Info) string { return string(databaseMessage) } -func SendFeishuNotification(notification Info, message, feishuWebHook string) error { +func SendFeishuNotification(notification *api.Info, message string) error { if api.MonitorType != "all" { - feishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLImportant"] + notification.FeishuWebHook = api.FeishuWebhookURLMap["FeishuWebhookURLImportant"] } messageIDMap := getMessageIDMap(notification.PerformanceType) @@ -202,7 +238,7 @@ func SendFeishuNotification(notification Info, message, feishuWebHook string) er } delete(messageIDMap, notification.DatabaseClusterName) } else { - if err := createFeishuNotification(notification, message, feishuWebHook, messageIDMap); err != nil { + if err := createFeishuNotification(notification, message, messageIDMap); err != nil { return err } } @@ -211,11 +247,11 @@ func SendFeishuNotification(notification Info, message, feishuWebHook string) er func getMessageIDMap(performanceType string) map[string]string { switch performanceType { - case "磁盘": + case api.DiskChinese: return api.DatabaseDiskMessageIDMap - case "内存": + case api.MemoryChinese: return api.DatabaseMemMessageIDMap - case "CPU": + case api.CPUChinese: return api.DatabaseCPUMessageIDMap case "Backup": return api.DatabaseBackupMessageIDMap @@ -231,8 +267,6 @@ func updateFeishuNotification(messageID, message string) error { MessageId(messageID). Body(larkim.NewPatchMessageReqBodyBuilder(). Content(message).Build()).Build() - - fmt.Println(messageID) resp, err := feiShuClient.Im.Message.Patch(context.Background(), req) if err != nil { log.Println("Error:", err) @@ -246,11 +280,11 @@ func updateFeishuNotification(messageID, message string) error { return nil } -func createFeishuNotification(notification Info, message, feishuWebHook string, messageIDMap map[string]string) error { +func createFeishuNotification(notification *api.Info, message string, messageIDMap map[string]string) error { req := larkim.NewCreateMessageReqBuilder(). ReceiveIdType("chat_id"). Body(larkim.NewCreateMessageReqBodyBuilder(). - ReceiveId(feishuWebHook). + ReceiveId(notification.FeishuWebHook). MsgType("interactive"). Content(message).Build()).Build() @@ -276,7 +310,6 @@ func createFeishuNotification(notification Info, message, feishuWebHook string, } else { messageIDMap[notification.DatabaseClusterName] = messageID } - fmt.Println(messageIDMap) return nil } @@ -317,7 +350,7 @@ func createCard(headerTemplate, headerTitle string, elements []map[string]string return card } -func GetQuotaMessage(nsQuota *NameSpaceQuota) string { +func GetQuotaMessage(nsQuota *api.NameSpaceQuota) string { var card map[string]interface{} elements := createQuotaElements(nsQuota) card = createCard("red", "Quota阀值通知", elements) @@ -330,7 +363,7 @@ func GetQuotaMessage(nsQuota *NameSpaceQuota) string { return databaseMessage } -func createQuotaElements(nsQuota *NameSpaceQuota) []map[string]string { +func createQuotaElements(nsQuota *api.NameSpaceQuota) []map[string]string { elements := []map[string]string{ {"label": "集群环境", "value": api.ClusterName}, {"label": "命名空间", "value": nsQuota.NameSpace}, @@ -339,7 +372,7 @@ func createQuotaElements(nsQuota *NameSpaceQuota) []map[string]string { return elements } -func addNonEmptyFieldsToElements(nsQuota *NameSpaceQuota, elements *[]map[string]string) { +func addNonEmptyFieldsToElements(nsQuota *api.NameSpaceQuota, elements *[]map[string]string) { fields := map[string]string{ "CPULimit": "CPU总量", "CPUUsage": "CPU使用率", diff --git a/service/exceptionmonitor/helper/notification/sms.go b/service/exceptionmonitor/helper/notification/sms.go index 5a50a3ceaef..ac1bd58c218 100644 --- a/service/exceptionmonitor/helper/notification/sms.go +++ b/service/exceptionmonitor/helper/notification/sms.go @@ -37,13 +37,13 @@ func GetPhoneNumberByNS(owner string) (string, error) { return phone, nil } -func SendToSms(namespace, databaseName, clusterName, content string) error { +func SendToSms(notificationInfo *api.Info, clusterName, content string) error { smsClient, err := utils.CreateSMSClient(os.Getenv("SMSAccessKeyID"), os.Getenv("SMSAccessKeySecret"), os.Getenv("SMSEndpoint")) if err != nil { return err } - name := strings.ReplaceAll(databaseName, "-", "/") - owner, _ := GetNSOwner(namespace) + name := strings.ReplaceAll(notificationInfo.DatabaseClusterName, "-", "/") + owner, _ := GetNSOwner(notificationInfo.Namespace) phoneNumbers, err := GetPhoneNumberByNS(owner) if err != nil { return err diff --git a/service/exceptionmonitor/main.go b/service/exceptionmonitor/main.go index fe518bc0006..209887d687f 100644 --- a/service/exceptionmonitor/main.go +++ b/service/exceptionmonitor/main.go @@ -19,6 +19,7 @@ func main() { go monitor.DatabasePerformanceMonitor() go monitor.DatabaseBackupMonitor() go monitor.QuotaMonitor() + go monitor.CockroachMonitor() select {} } diff --git a/service/go.work.sum b/service/go.work.sum index dce39ff6b4c..08f064570c8 100644 --- a/service/go.work.sum +++ b/service/go.work.sum @@ -1,3 +1,4 @@ +cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -117,6 +118,7 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.50.0 h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= @@ -233,6 +235,7 @@ cloud.google.com/go/dataqna v0.8.1/go.mod h1:zxZM0Bl6liMePWsHA8RMGAfmTG34vJMapbH cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= cloud.google.com/go/dataqna v0.8.4/go.mod h1:mySRKjKg5Lz784P6sCov3p1QD+RZQONRMRjzGNcFd0c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= @@ -291,6 +294,7 @@ cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466d cloud.google.com/go/filestore v1.7.1/go.mod h1:y10jsorq40JJnjR/lQ8AfFbbcGlw3g+Dp8oN7i7FjV4= cloud.google.com/go/filestore v1.8.0 h1:/+wUEGwk3x3Kxomi2cP5dsR8+SIXxo7M0THDjreFSYo= cloud.google.com/go/filestore v1.8.0/go.mod h1:S5JCxIbFjeBhWMTfIYH2Jx24J6BqjwpkkPl+nBA5DlI= +cloud.google.com/go/firestore v1.1.0 h1:9x7Bx0A9R5/M9jibeJeZWqjeVEIxYW9fZYqB9a70/bY= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= @@ -381,6 +385,7 @@ cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHS cloud.google.com/go/longrunning v0.5.2/go.mod h1:nqo6DQbNV2pXhGDbDMoN2bWz68MjZUzqv2YttZiveCs= cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= cloud.google.com/go/managedidentities v1.5.0 h1:ZRQ4k21/jAhrHBVKl/AY7SjgzeJwG1iZa+mJ82P+VNg= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= @@ -475,6 +480,7 @@ cloud.google.com/go/privatecatalog v0.9.4/go.mod h1:SOjm93f+5hp/U3PqMZAHTtBtluqL cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.30.0 h1:vCge8m7aUKBJYOgrZp7EsNDf6QMd2CAlXZqWTn3yq6s= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= @@ -576,6 +582,7 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= @@ -611,6 +618,7 @@ cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV cloud.google.com/go/translate v1.8.2/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= cloud.google.com/go/translate v1.10.0 h1:tncNaKmlZnayMMRX/mMM2d5AJftecznnxVBD4w070NI= cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= +cloud.google.com/go/translate v1.10.3 h1:g+B29z4gtRGsiKDoTF+bNeH25bLRokAaElygX2FcZkE= cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= cloud.google.com/go/video v1.15.0 h1:upIbnGI0ZgACm58HPjAeBMleW3sl5cT84AbYQ8PWOgM= cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= @@ -659,6 +667,7 @@ cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcP cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g= cloud.google.com/go/workflows v1.12.3 h1:qocsqETmLAl34mSa01hKZjcqAvt699gaoFbooGGMvaM= cloud.google.com/go/workflows v1.12.3/go.mod h1:fmOUeeqEwPzIU81foMjTRQIdwQHADi/vEr1cx9R1m5g= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= @@ -677,6 +686,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -722,8 +732,11 @@ github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apecloud/kubeblocks v0.8.4 h1:8esK2e9iiziPXTlGXmX2uFTU/YGFXFvyvqnCBODqWM4= github.com/apecloud/kubeblocks v0.8.4/go.mod h1:xQpzfMy4V+WJI5IKBWB02qsKAlVR3nAE71CPkAs2uOs= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= @@ -731,18 +744,27 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ= github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug= github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY= github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8= github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -763,8 +785,11 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= @@ -795,6 +820,7 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= @@ -819,6 +845,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= @@ -843,13 +870,16 @@ github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1: github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -862,8 +892,10 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= @@ -880,8 +912,11 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= @@ -893,9 +928,11 @@ github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -903,6 +940,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -933,10 +971,13 @@ github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3 github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= @@ -953,6 +994,7 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= @@ -991,40 +1033,58 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjd github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c h1:rwmN+hgiyp8QyBqzdEX43lTjKAxaqCrYHaU5op5P9J8= github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -1039,6 +1099,7 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -1046,9 +1107,11 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= @@ -1058,11 +1121,14 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/labring/operator-sdk v1.0.1 h1:JS+j9nF0lihkPJnMYJBZrH7Kfp/dKB2cnbBRMfkmE+g= github.com/labring/operator-sdk v1.0.1/go.mod h1:velfQ6SyrLXBeAShetQyR7q1zJNd8vGO6jjzbKcofj8= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE= github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= @@ -1070,11 +1136,13 @@ github.com/lyft/protoc-gen-star/v2 v2.0.1 h1:keaAo8hRuAT0O3DfJ/wM3rufbAjGeJ1lAtW github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= @@ -1083,6 +1151,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= @@ -1098,10 +1167,15 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -1123,7 +1197,9 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -1147,7 +1223,9 @@ github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bl github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -1160,8 +1238,11 @@ github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -1174,6 +1255,7 @@ github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdU github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= @@ -1194,10 +1276,13 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 h1:RpforrEYXWkmGwJHIGnLZ3tTWStkjVVstwzNGqxX2Ds= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= @@ -1210,14 +1295,15 @@ github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHE github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -1231,6 +1317,7 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= @@ -1248,6 +1335,7 @@ github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1Fof github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1374,6 +1462,7 @@ go.opentelemetry.io/otel/sdk v1.10.0 h1:jZ6K7sVn04kk/3DNUdJ4mqRlGDiXAVuIG+MMENpT go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE= go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.10.0 h1:npQMbR8o7mum8uF95yFbOEJffhs1sbCOfDh8zAJiH5E= go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM= @@ -1436,6 +1525,7 @@ golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -1489,6 +1579,7 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1559,7 +1650,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1575,9 +1665,12 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1647,6 +1740,7 @@ golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= @@ -1744,6 +1838,8 @@ google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrq google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -1760,7 +1856,9 @@ google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cba google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405 h1:o4S3HvTUEXgRsNSUQsALDVog0O9F/U1JJlHmmUN8Uas= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20241021214115-324edc3d5d38 h1:42FIsHvG4GOaVHLDMcy/YMqC4clCbgAPojbcA2hXp5w= google.golang.org/genproto/googleapis/bytestream v0.0.0-20241021214115-324edc3d5d38/go.mod h1:T8O3fECQbif8cez15vxAcjbwXxvL2xbnvbQ7ZfiMAMs= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20241118233622-e639e219e697/go.mod h1:qUsLYwbwz5ostUWtuFuXPlHmSJodC5NI/88ZlHj4M1o= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= @@ -1775,6 +1873,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1833,6 +1933,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4= k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y= @@ -1876,13 +1977,21 @@ k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 h1:trsWhjU5jZrx6UvFu4WzQDrN7Pga4a7Qg+zcfcj64PA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2/go.mod h1:+qG7ISXqCDVVcyO8hLn12AKVYYUjM7ftlqsqmrhMZE0=