diff --git a/example/s.yaml b/example/s.yaml new file mode 100644 index 00000000..129c0d6d --- /dev/null +++ b/example/s.yaml @@ -0,0 +1,68 @@ +edition: 3.0.0 +name: ai-model-app +access: quanxi + +resources: + modelDemo: + component: fc3@dev + props: + region: cn-shanghai + runtime: custom-container + functionName: ${env('fc_component_function_name', 'ai-model-test-qwen')} + description: model service from functionai test + logConfig: auto + vpcConfig: auto + nasConfig: auto + instanceConcurrency: 20 + cpu: 8 + memorySize: 65536 + diskSize: 10240 + timeout: 300 + gpuConfig: + gpuMemorySize: 49152 + gpuType: fc.gpu.ada.1 + customContainerConfig: + image: >- + serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai/dms-vllm:openai_v0.10.0 + port: 9000 + entrypoint: + - vllm + - serve + - /mnt/${env('fc_component_function_name', 'ai-model-test-qwen')}/${env('MODEL_ID', 'Qwen/Qwen3-32B-AWQ')} + - --port + - "9000" + - --served-model-name + - ${env('MODEL_ID', 'Qwen/Qwen3-32B-AWQ')} + # - Qwen/Qwen3-14B + - --tensor-parallel-size + - "1" + - --trust-remote-code # 添加这个参数 + - --max-model-len + - "4096" + triggers: # 默认,用户可能关注的是开启 authType 是 bear token + - triggerConfig: + methods: + - GET + - POST + - PUT + - DELETE + authType: anonymous + disableURLInternet: false + triggerName: httpTrigger + description: '' + qualifier: LATEST + triggerType: http + provisionConfig: + target: 1 + alwaysAllocateCPU: false + alwaysAllocateGPU: false + mode: sync + + supplement: + modelConfig: + source: modelscope + id: ${env('MODEL_ID', 'Qwen/Qwen3-32B-AWQ')} + # id: Qwen/Qwen3-14B + storage: nas + role: acs:ram::${config('AccountID')}:role/aliyundevsdefaultrole + diff --git a/example/test_models.sh b/example/test_models.sh new file mode 100755 index 00000000..eea0cea3 --- /dev/null +++ b/example/test_models.sh @@ -0,0 +1,125 @@ +#!/bin/bash + +# 定义日志文件 +LOG_FILE="test_models.log" + +# 清空或创建日志文件 +> "$LOG_FILE" + +# 日志记录函数 +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +# 定义model_id列表 +MODEL_IDS=("Qwen/Qwen2-Audio-7B-Instruct" "Qwen/Qwen2.5-0.5B-Instruct" "Qwen/Qwen3-32B-AWQ" "Qwen/Qwen3-14B" "Qwen/QwQ-32B-AWQ") + +# 检查yaml文件是否存在 +YAML_FILE="s.yaml" +if [ ! -f "$YAML_FILE" ]; then + log "Error: $YAML_FILE not found!" + exit 1 +fi + +# 遍历每个model_id +for MODEL_ID in "${MODEL_IDS[@]}"; do + log "========================================" + log "Testing model: $MODEL_ID" + log "========================================" + + # 生成随机函数名 + RANDOM_STRING=$(openssl rand -hex 16) + export fc_component_function_name=ai-model-qwen-$RANDOM_STRING + export NEW_MODEL_SERVICE_CLIENT_CONNECT_TIMEOUT=10000 + export MODEL_ID=$MODEL_ID + + # 下载模型 + log "Downloading model..." + DOWNLOAD_OUTPUT=$(s model download 2>&1) + echo "$DOWNLOAD_OUTPUT" >> "$LOG_FILE" + echo "$DOWNLOAD_OUTPUT" + if echo "$DOWNLOAD_OUTPUT" | grep -q "Error"; then + log "Failed to download model: $MODEL_ID" + continue + fi + + # 部署服务 + log "Deploying..." + DEPLOY_OUTPUT=$(s deploy 2>&1) + echo "$DEPLOY_OUTPUT" >> "$LOG_FILE" + echo "$DEPLOY_OUTPUT" + + # 检查部署是否成功 + if echo "$DEPLOY_OUTPUT" | grep -q "state:.*Active"; then + log "Deployment successful for model: $MODEL_ID" + else + log "Deployment failed for model: $MODEL_ID" + # 清理资源 + REMOVE_OUTPUT=$(s model remove -y 2>&1) + echo "$REMOVE_OUTPUT" >> "$LOG_FILE" + echo "$REMOVE_OUTPUT" + REMOVE_OUTPUT=$(s remove -y 2>&1) + echo "$REMOVE_OUTPUT" >> "$LOG_FILE" + echo "$REMOVE_OUTPUT" + continue + fi + + # 提取system_url + SYSTEM_URL=$(echo "$DEPLOY_OUTPUT" | grep "system_url:" | sed 's/.*system_url: *//' | tr -d ' "[:cntrl:]') + if [ -z $SYSTEM_URL ]; then + log "Failed to extract system_url for model: $MODEL_ID" + # 清理资源 + REMOVE_OUTPUT=$(s model remove -y 2>&1) + echo "$REMOVE_OUTPUT" >> "$LOG_FILE" + echo "$REMOVE_OUTPUT" + REMOVE_OUTPUT=$(s remove -y 2>&1) + echo "$REMOVE_OUTPUT" >> "$LOG_FILE" + echo "$REMOVE_OUTPUT" + continue + fi + log "Extracted system_url: $SYSTEM_URL" + + # 发送测试请求 + log "Sending test request..." + CURL_OUTPUT=$(curl --request POST \ + --url "$SYSTEM_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + --data '{ + "model": "$MODEL_ID", + "messages": [ + { + "role": "user", + "content": "Hello! 你是谁?" + } + ], + "stream": false + }' 2>&1) + + echo "$CURL_OUTPUT" >> "$LOG_FILE" + echo "$CURL_OUTPUT" + + # 检查curl请求是否成功 + if echo "$CURL_OUTPUT" | grep -q '"object":"chat.completion"'; then + log "Model test successful for: $MODEL_ID" + # 提取并显示模型回复内容 + RESPONSE_CONTENT=$(echo "$CURL_OUTPUT" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | sed 's/\\n/\n/g' | sed 's/\\t/\t/g') + log "Model response: $RESPONSE_CONTENT" + else + log "Model test failed for: $MODEL_ID" + fi + + # 清理资源 + log "Removing resources..." + REMOVE_OUTPUT=$(s model remove -y 2>&1) + echo "$REMOVE_OUTPUT" >> "$LOG_FILE" + echo "$REMOVE_OUTPUT" + REMOVE_OUTPUT=$(s remove -y 2>&1) + echo "$REMOVE_OUTPUT" >> "$LOG_FILE" + echo "$REMOVE_OUTPUT" + + log "" + log "Finished testing model: $MODEL_ID" + log "" +done + +log "All models tested." \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1ad2d3e1..1092727f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "ISC", "dependencies": { + "@alicloud/devs20230714": "^2.4.5", "@alicloud/fc2": "^2.6.6", "@alicloud/fc20230330": "4.3.4", "@alicloud/pop-core": "^1.8.0", @@ -151,6 +152,16 @@ "@alicloud/tea-typescript": "^1.5.1" } }, + "node_modules/@alicloud/devs20230714": { + "version": "2.4.5", + "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@alicloud/devs20230714/-/@alicloud/devs20230714-2.4.5.tgz", + "integrity": "sha512-4gBq5S014v9jICo/asDbpcXIULaR4aqjB4E4IqDvweIJzmMXvENkHeZbklZNqzep5OmTUCJPFNqJ1dVhohjHsQ==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/openapi-core": "^1.0.0", + "@darabonba/typescript": "^1.0.0" + } + }, "node_modules/@alicloud/endpoint-util": { "version": "0.0.1", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", @@ -16033,6 +16044,15 @@ "@alicloud/tea-typescript": "^1.5.1" } }, + "@alicloud/devs20230714": { + "version": "2.4.5", + "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@alicloud/devs20230714/-/@alicloud/devs20230714-2.4.5.tgz", + "integrity": "sha512-4gBq5S014v9jICo/asDbpcXIULaR4aqjB4E4IqDvweIJzmMXvENkHeZbklZNqzep5OmTUCJPFNqJ1dVhohjHsQ==", + "requires": { + "@alicloud/openapi-core": "^1.0.0", + "@darabonba/typescript": "^1.0.0" + } + }, "@alicloud/endpoint-util": { "version": "0.0.1", "resolved": "https://packages.aliyun.com/670e108663cd360abfe4be65/npm/npm-registry/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", diff --git a/package.json b/package.json index e3c261fa..85bd0a87 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "author": "", "license": "ISC", "dependencies": { + "@alicloud/devs20230714": "^2.4.5", "@alicloud/fc2": "^2.6.6", "@alicloud/fc20230330": "4.3.4", "@alicloud/pop-core": "^1.8.0", @@ -34,6 +35,7 @@ "@serverless-devs/utils": "^0.0.17", "@serverless-devs/zip": "^0.0.3-beta.8", "ajv": "^8.17.1", + "ali-oss": "6.18.1", "aliyun-sdk": "^1.12.10", "chalk": "^4.1.0", "crc64-ecma182.js": "^2.0.2", @@ -51,8 +53,7 @@ "temp-dir": "^2.0.0", "tty-table": "^4.2.3", "uuid": "^9.0.1", - "uuid-by-string": "^4.0.0", - "ali-oss": "6.18.1" + "uuid-by-string": "^4.0.0" }, "devDependencies": { "@serverless-devs/component-interface": "^0.0.6", diff --git a/src/commands-help/model.ts b/src/commands-help/model.ts new file mode 100644 index 00000000..4b537ac0 --- /dev/null +++ b/src/commands-help/model.ts @@ -0,0 +1,22 @@ +export default { + help: { + description: 'Resource layer operation ', + summary: 'Resource layer operation ', + }, + subCommands: { + download: { + help: { + description: `Download model`, + summary: 'Publish new layer version', + option: [], + }, + }, + reomve: { + help: { + description: `Remove model`, + summary: 'Remove model', + option: [], + }, + }, + }, +}; diff --git a/src/default/resources.ts b/src/default/resources.ts index 7b2fd3ae..6ba7c600 100644 --- a/src/default/resources.ts +++ b/src/default/resources.ts @@ -4,3 +4,10 @@ export const VPC_AND_NAS_NAME = process.env.FC_GENERATE_VPC_AND_NAS_NAME || defa const defaultLogStoreName = 'default-logs'; export const PROJECT = process.env.FC_GENERATE_PROJECT_NAME; export const LOG_STORE = process.env.FC_GENERATE_LOGSTORE_NAME || defaultLogStoreName; + +export function getEnvVariable(key: string): string { + if (typeof process !== 'undefined' && process.env) { + return process.env[key] || ''; + } + return ''; +} diff --git a/src/index.ts b/src/index.ts index 378e40fc..452e9c1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ import SYaml2To3 from './subCommands/2to3'; import Logs from './subCommands/logs'; import { SCHEMA_FILE_PATH } from './constant'; import { checkDockerIsOK, isAppCenter, isYunXiao } from './utils'; +import { Model } from './subCommands/model'; export default class Fc extends Base { // 部署函数 @@ -168,6 +169,13 @@ export default class Fc extends Base { return await logs.run(); } + public async model(inputs: IInputs) { + await super.handlePreRun(inputs, false); + const model = new Model(inputs); + logger.debug(`model inputs: ${model.subCommand}`); + return await model[model.subCommand](); + } + public async getSchema(inputs: IInputs) { logger.debug(`getSchema: ${JSON.stringify(inputs)}`); return fs.readFileSync(SCHEMA_FILE_PATH, 'utf-8'); diff --git a/src/interface/index.ts b/src/interface/index.ts index 1e2a9aa0..8fd7d021 100644 --- a/src/interface/index.ts +++ b/src/interface/index.ts @@ -19,6 +19,7 @@ export interface IProps extends IFunction { concurrencyConfig?: IConcurrencyConfig; provisionConfig?: IProvisionConfig; endpoint?: string; + supplement?: any; } export interface IInputs extends _IInputs { diff --git a/src/subCommands/deploy/impl/function.ts b/src/subCommands/deploy/impl/function.ts index c7753ab9..7d2d6333 100644 --- a/src/subCommands/deploy/impl/function.ts +++ b/src/subCommands/deploy/impl/function.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _, { isEmpty } from 'lodash'; import { diffConvertYaml } from '@serverless-devs/diff'; import inquirer from 'inquirer'; import fs from 'fs'; @@ -319,7 +319,7 @@ export default class Service extends Base { * 生成 auto 资源,非 FC 资源,主要指 vpc、nas、log、role(oss mount 挂载点才有) */ private async _deployAuto() { - const { region } = this.inputs.props; + const { region, supplement } = this.inputs.props; const { credential } = this.inputs; const { functionName } = this.local; @@ -390,14 +390,19 @@ vpcConfig: _.set(this.local, 'vpcConfig', vpcConfig); } if (nasAuto) { + let modelConfig; + if (!isEmpty(supplement)) { + modelConfig = supplement.modelConfig; + } + logger.write( yellow(`Created nas resource succeeded, please replace nasConfig: auto in yaml with: nasConfig: groupId: 0 userId: 0 mountPoints: - - serverAddr: ${mountTargetDomain}:/${functionName} - mountDir: /mnt/${functionName} + - serverAddr: ${mountTargetDomain}:/${functionName}${isEmpty(modelConfig) ? '' : '/' + modelConfig.id} + mountDir: /mnt/${functionName}${isEmpty(modelConfig) ? '' : '/' + modelConfig.id} enableTLS: false\n`), ); this.createResource.nas = { mountTargetDomain, fileSystemId }; @@ -406,8 +411,8 @@ nasConfig: userId: 0, mountPoints: [ { - serverAddr: `${mountTargetDomain}:/${functionName}`, - mountDir: `/mnt/${functionName}`, + serverAddr: `${mountTargetDomain}:/${functionName}${isEmpty(modelConfig) ? '' : '/' + modelConfig.id}`, + mountDir: `/mnt/${functionName}${isEmpty(modelConfig) ? '' : '/' + modelConfig.id}`, enableTLS: false, }, ], diff --git a/src/subCommands/model/index.ts b/src/subCommands/model/index.ts new file mode 100644 index 00000000..d00bec45 --- /dev/null +++ b/src/subCommands/model/index.ts @@ -0,0 +1,330 @@ +import { IFunction, IInputs } from '../../interface'; +import logger from '../../logger'; +import _, { isEmpty } from 'lodash'; +import FC from '../../resources/fc'; +import VPC_NAS from '../../resources/vpc-nas'; +import { ICredentials } from '@serverless-devs/component-interface'; +import { yellow } from 'chalk'; +import DevClient, * as devs from '@alicloud/devs20230714'; +import * as $OpenApi from '@alicloud/openapi-client'; +import { getEnvVariable } from '../../default/resources'; +import commandsHelp from '../../commands-help/layer'; +import { parseArgv } from '@serverless-devs/utils'; +import assert from 'assert'; +import { sleep } from '../../utils'; + +export const NEW_MODEL_SERVICE_CLIENT_CONNECT_TIMEOUT: number = + parseInt(process.env.NEW_MODEL_SERVICE_CLIENT_CONNECT_TIMEOUT as string, 10) || 5 * 1000; +export const NEW_MODEL_SERVICE_CLIENT_READ_TIMEOUT: number = + parseInt(process.env.NEW_MODEL_SERVICE_CLIENT_READ_TIMEOUT as string, 10) || 86400 * 1000; +export const MODEL_DOWNLOAD_TIMEOUT: number = + parseInt(process.env.MODEL_DOWNLOAD_TIMEOUT as string, 10) || 42 * 60 * 1000; + +const commandsList = Object.keys(commandsHelp.subCommands); + +export class Model { + readonly subCommand: string; + createResource: Record = {}; + logger = logger; + local: IFunction; + projectName: string; + envName: string; + + constructor(private inputs: IInputs) { + this.logger.debug( + `inputs params: ${JSON.stringify(this.inputs.props)}, args: ${this.inputs.args}`, + ); + + const opts = parseArgv(inputs.args, { + alias: { help: 'h', 'assume-yes': 'y' }, + boolean: ['help'], + string: [], + }); + const { _: subCommands } = opts; + logger.debug(`subCommands: ${JSON.stringify(subCommands, null, 2)}`); + const subCommand = _.get(subCommands, '[0]'); + if (!subCommand || !commandsList.includes(subCommand)) { + throw new Error( + `Command "${subCommand}" not found, Please use "s cli fc3 layer -h" to query how to use the command`, + ); + } + this.subCommand = subCommand; + this.local = _.cloneDeep(inputs.props); + } + + async download() { + // 1. auto ---> auto 包, 有 nasConfig + // 2. 调用 download 接口,若返回一个错误是下载服务已经存在,继续等待 get 轮询。 + // 3. 轮询 get 接口 + const { credential } = this.inputs; + const { region, supplement } = this.inputs.props; + const { functionName } = this.local; + const { modelConfig } = supplement; + + if (isEmpty(modelConfig)) { + logger.error(`[Download-model] modelConfig is empty.`); + throw new Error(`[Download-model] modelConfig is empty.`); + } + + logger.info(`[Download-model] Download model start.`); + + // 混合更多因子,防止重复 + const projectName = getEnvVariable('ALIYUN_DEVS_REMOTE_PROJECT_NAME'); + const envName = getEnvVariable('ALIYUN_DEVS_REMOTE_ENV_NAME'); + + this.projectName = projectName; + this.envName = envName; + const name = `${projectName}$${envName}$${functionName}`; + + const { nasAuto, vpcAuto } = FC.computeLocalAuto(this.local); + logger.debug(`[nasAuto] Auto compute local auto, nasAuto: ${nasAuto};`); + if (nasAuto || vpcAuto) { + const client = new VPC_NAS(region, credential as ICredentials); + const localVpcAuto = _.isString(this.local.vpcConfig) ? undefined : this.local.vpcConfig; + // @ts-ignore: nas auto 会返回 mountTargetDomain 和 fileSystemId + const { vpcConfig, mountTargetDomain, fileSystemId } = await client.deploy({ + nasAuto, + vpcConfig: localVpcAuto, + }); + + if (vpcAuto) { + const { vSwitchIds } = vpcConfig; + this._assertArrayOfStrings(vSwitchIds); + const vSwitchIdsArray: string[] = vSwitchIds as string[]; + logger.write( + yellow(`[nasAuto] Created vpc resource succeeded, please manually write vpcConfig to the yaml file: +vpcConfig: + vpcId: ${vpcConfig.vpcId} + securityGroupId: ${vpcConfig.securityGroupId} + vSwitchIds: + - ${vSwitchIdsArray.join(' - \n')}\n`), + ); + this.createResource.vpc = vpcConfig; + _.set(this.local, 'vpcConfig', vpcConfig); + logger.info('[nasAuto] vpcAuto finished.'); + } + if (nasAuto) { + logger.write( + yellow(`[nasAuto] Created nas resource succeeded, please replace nasConfig: auto in yaml with: +nasConfig: +groupId: 0 +userId: 0 +mountPoints: + - serverAddr: ${mountTargetDomain}:/${functionName}/${supplement.modelConfig.id} + mountDir: /mnt/${functionName}/${supplement.modelConfig.id} + enableTLS: false\n`), + ); + this.createResource.nas = { mountTargetDomain, fileSystemId }; + _.set(this.local, 'nasConfig', { + groupId: 0, + userId: 0, + mountPoints: [ + { + serverAddr: `${mountTargetDomain}:/${functionName}/${supplement.modelConfig.id}`, + mountDir: `/mnt/${functionName}/${supplement.modelConfig.id}`, + enableTLS: false, + }, + ], + }); + } + } + + // downloadModel + const devClient = await this.getNewModelServiceClient(); + + if (devClient) { + const { nasConfig, vpcConfig } = this.local; + logger.debug(`[Download-model] nasConfig: ${nasConfig} vpcConfig: ${vpcConfig}`); + const { nasMountDomain, nasMountPath } = this.parseNasConfig(nasConfig); + let resp; + if (typeof nasConfig === 'object' && nasConfig?.mountPoints) { + const params = { + modelConfig: { + model: modelConfig.id, + type: modelConfig.source, + reversion: modelConfig.version, + token: modelConfig.token, + bucket: modelConfig.ossBucket, + path: modelConfig.ossPath, + region: modelConfig.ossRegion, + }, + region, + modelPath: nasMountPath, + role: modelConfig.role, + nasMountPoint: nasMountDomain, + vpcConfig, + }; + try { + const req = new devs.DownloadModelRequest(params); + logger.debug(req); + resp = await devClient.downloadModel(name, req); + logger.debug(resp); + } catch (e) { + logger.error(`download model invocation error: ${e.message}`); + throw new Error(`download model error: ${e.message}`); + } + + if (resp.statusCode != 200 && resp.statusCode != 202) { + logger.info({ status: resp.statusCode, body: resp.body }); + throw new Error( + `download model connection error, statusCode: ${resp.statusCode}, body: ${resp.body}`, + ); + } + + const rb = resp.body; + if (rb.success || rb.errMsg.includes('is already exist')) { + logger.info(`download model requestId: ${rb.requestId}`); + while (true) { + const modelStatus = await this.getModelStatus(devClient, name); + + if (modelStatus.finished) { + const durationMs = modelStatus.finishedTime - modelStatus.startTime; + logger.warn(`Time taken for model download: ${durationMs}.`); + logger.info(`[Download-model] Download model finished.`); + return true; + } + + if (Date.now() - modelStatus.startTime > MODEL_DOWNLOAD_TIMEOUT) { + const errorMessage = `[Model-download] Download timeout after ${ + MODEL_DOWNLOAD_TIMEOUT / 1000 / 60 + } minutes`; + throw new Error(errorMessage); + } + + logger.warn(`[Download-model] Download task not finished yet, polling in 10 seconds`); + await sleep(10); + } + } else { + throw new Error( + `download model service biz failed, errCode: ${rb.errCode}, errMsg: ${rb.errMsg}`, + ); + } + } + } + } + + async getNewModelServiceClient(): Promise { + const { + AccessKeyID: accessKeyId, + AccessKeySecret: accessKeySecret, + SecurityToken: securityToken, + } = await this.inputs.getCredential(); + + let endpoint: string; + + endpoint = 'devs.cn-hangzhou.aliyuncs.com'; + if (process.env.ARTIFACT_ENDPOINT) { + endpoint = process.env.ARTIFACT_ENDPOINT; + } + if (process.env.artifact_endpoint) { + endpoint = process.env.artifact_endpoint; + } + + const protocol = 'https'; + + const config = new $OpenApi.Config({ + accessKeyId, + accessKeySecret, + securityToken, + protocol, + endpoint, + readTimeout: NEW_MODEL_SERVICE_CLIENT_READ_TIMEOUT, + connectTimeout: NEW_MODEL_SERVICE_CLIENT_CONNECT_TIMEOUT, + userAgent: `${ + this.inputs.userAgent || + `Component:cap-model;Nodejs:${process.version};OS:${process.platform}-${process.arch}` + }`, + }); + + logger.info(`new models service init, ARTIFACT_ENDPOINT endpoint: ${config.endpoint}`); + + return new DevClient(config); + } + + private _assertArrayOfStrings(variable: any) { + assert(Array.isArray(variable), 'Variable must be an array'); + assert( + variable.every((item) => typeof item === 'string'), + 'Variable must contain only strings', + ); + } + + async getModelStatus(client: DevClient, name: string) { + let resp; + try { + resp = await client.getModelStatus(name); + logger.debug(resp); + } catch (e) { + logger.error(`[Download-model] get model status error: ${e.message} for model ${name}`); + throw new Error(`[Download-model] get model status error: ${e.message} for model ${name}`); + } + + if (resp.statusCode != 200 && resp.statusCode != 202) { + logger.info({ status: resp.statusCode, body: resp.body }); + throw new Error( + `[Download-model] get model status connection error, statusCode: ${resp.statusCode}, body: ${resp.body}`, + ); + } + + const rb = resp.body; + if (rb.success) { + return rb.data; + } else { + throw new Error( + `[Download-model] get model status biz failed, errCode: ${rb.errCode}, errMsg: ${rb.errMsg}`, + ); + } + } + + private parseNasConfig(nasConfig: any) { + let nasMountDomain: string; + let nasMountPath: string = ''; + + let serverAddr = nasConfig.mountPoints[0].serverAddr; + const parts = serverAddr.split(':', 2); + + if (parts.length === 2) { + [nasMountDomain, nasMountPath] = parts; + } else { + throw new Error('nasConfig serverAddr string does not contain a colon'); + } + + return { nasMountDomain, nasMountPath }; + } + + async remove() { + logger.info('[Remove-model] remove model ...'); + const { functionName } = this.inputs.props; + const projectName = getEnvVariable('ALIYUN_DEVS_REMOTE_PROJECT_NAME'); + const envName = getEnvVariable('ALIYUN_DEVS_REMOTE_ENV_NAME'); + const name = `${projectName}$${envName}$${functionName}`; + + const devClient = await this.getNewModelServiceClient(); + + try { + const resp = await devClient.deleteModel(name); + logger.debug(resp); + + if (resp.statusCode !== 200 && resp.statusCode !== 202) { + logger.info({ status: resp.statusCode, body: resp.body }); + throw new Error( + `[Remove-model] delete model connection error, statusCode: ${resp.statusCode}, body: ${resp.body}`, + ); + } + + const rb = resp.body; + if (rb.success) { + logger.info(`[Remove-model] delete model requestId: ${rb.requestId}`); + logger.info(`[Remove-model] Remove model succeeded.`); + return true; + } else { + throw new Error( + `[Remove-model] delete model service biz failed, errCode: ${rb.errCode}, errMsg: ${rb.errMsg}`, + ); + } + } catch (e) { + logger.error(`[Remove-model] delete model invocation error: ${e.message}`); + throw new Error(`[Remove-model] delete model error: ${e.message}`); + } + } +}