返回 AiToEarn
volcengine.exception.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / libs / volcengine / volcengine.exception.ts
1 import type { Locale } from '@yikart/common'
2 import type { AxiosError } from 'axios'
3 import { HttpException, HttpStatus } from '@nestjs/common'
4 import { getErrorMessage, getLocale } from '@yikart/common'
5 import { isAxiosError } from 'axios'
6
7 interface VolcengineErrorPayload {
8 code?: string | number
9 message?: string
10 param?: string
11 type?: string
12 }
13
14 interface VolcengineErrorResponseData {
15 error?: VolcengineErrorPayload
16 }
17
18 interface VolcengineExceptionInput {
19 operation: string
20 httpStatus?: number
21 providerCode?: string | number
22 providerMessage?: string
23 providerType?: string
24 providerParam?: string
25 requestId?: string
26 raw?: unknown
27 }
28
29 type OfficialMessage = Record<Locale, string>
30
31 const DEFAULT_MESSAGE: OfficialMessage = {
32 'en-US': 'Volcengine request failed',
33 'zh-CN': '火山引擎请求失败',
34 }
35
36 const OFFICIAL_MESSAGE_BY_CODE: Record<string, OfficialMessage> = {
37 'MissingParameter': {
38 'en-US': 'The request failed because it is missing one or multiple required parameters.',
39 'zh-CN': '请求缺少必要参数,请查阅 API 文档。',
40 },
41 'InvalidParameter': {
42 'en-US': 'One or more parameters specified in the request are not valid.',
43 'zh-CN': '请求包含非法参数,请查阅 API 文档。',
44 },
45 'InvalidEndpoint.ClosedEndpoint': {
46 'en-US': 'The request targeted an endpoint that is currently closed or temporarily unavailable.',
47 'zh-CN': '推理接入点处于已被关闭或暂时不可用,请稍后重试,或联系推理接入点管理员。',
48 },
49 'SensitiveContentDetected': {
50 'en-US': 'The request failed because the input text may contain sensitive information.',
51 'zh-CN': '输入文本可能包含敏感信息,请您使用其他 prompt。',
52 },
53 'SensitiveContentDetected.SevereViolation': {
54 'en-US': 'The request failed because the input text may contain severe violation information.',
55 'zh-CN': '输入文本可能包含严重违规相关信息,请您使用其他 prompt。',
56 },
57 'SensitiveContentDetected.Violence': {
58 'en-US': 'The request failed because the input text may contain violence information.',
59 'zh-CN': '输入文本可能包含激进行为相关信息,请您使用其他 prompt。',
60 },
61 'InputTextSensitiveContentDetected': {
62 'en-US': 'The request failed because the input text may contain sensitive information.',
63 'zh-CN': '输入文本可能包含敏感信息,请您更换后重试。',
64 },
65 'InputImageSensitiveContentDetected': {
66 'en-US': 'The request failed because the input image may contain sensitive information.',
67 'zh-CN': '输入图片可能包含敏感信息,请您更换后重试。',
68 },
69 'InputVideoSensitiveContentDetected': {
70 'en-US': 'The request failed because the input video may contain sensitive information.',
71 'zh-CN': '输入视频可能包含敏感信息,请您更换后重试。',
72 },
73 'InputAudioSensitiveContentDetected': {
74 'en-US': 'The request failed because the input audio may contain sensitive information.',
75 'zh-CN': '输入音频可能包含敏感信息,请您更换后重试。',
76 },
77 'OutputTextSensitiveContentDetected': {
78 'en-US': 'The request failed because the output may contain sensitive information.',
79 'zh-CN': '生成的文字可能包含敏感信息,请您更换输入内容后重试。',
80 },
81 'OutputImageSensitiveContentDetected': {
82 'en-US': 'The request failed because the output image may contain sensitive information.',
83 'zh-CN': '生成的图像可能包含敏感信息,请您更换输入内容后重试。',
84 },
85 'OutputVideoSensitiveContentDetected': {
86 'en-US': 'The request failed because the output video may contain sensitive information.',
87 'zh-CN': '生成的视频可能包含敏感信息,请您更换输入内容后重试。',
88 },
89 'OutputAudioSensitiveContentDetected': {
90 'en-US': 'The request failed because the output audio may contain sensitive information.',
91 'zh-CN': '生成的音频可能包含敏感信息,请您更换输入内容后重试。',
92 },
93 'InputTextSensitiveContentDetected.PolicyViolation': {
94 'en-US': 'The request failed because the input text may violate platform rules.',
95 'zh-CN': '输入文本可能违反平台规定,请您更换后重试。',
96 },
97 'InputImageSensitiveContentDetected.PolicyViolation': {
98 'en-US': 'The request failed because the input image may violate platform rules.',
99 'zh-CN': '输入图片可能违反平台规定,请您更换后重试。',
100 },
101 'InputVideoSensitiveContentDetected.PolicyViolation': {
102 'en-US': 'The request failed because the input video may violate platform rules.',
103 'zh-CN': '输入视频可能违反平台规定,请您更换后重试。',
104 },
105 'InputAudioSensitiveContentDetected.PolicyViolation': {
106 'en-US': 'The request failed because the input audio may violate platform rules.',
107 'zh-CN': '输入音频可能违反平台规定,请您更换后重试。',
108 },
109 'InputImageSensitiveContentDetected.PrivacyInformation': {
110 'en-US': 'The request failed because the input image may contain real person.',
111 'zh-CN': '输入图片可能包含真人,请您更换后重试。',
112 },
113 'InputVideoSensitiveContentDetected.PrivacyInformation': {
114 'en-US': 'The request failed because the input video may contain real person.',
115 'zh-CN': '输入视频可能包含真人,请您更换后重试。',
116 },
117 'InputTextRiskDetection': {
118 'en-US': 'The request could not be processed because the input text includes sensitive content that violates ContentSecurityDetection.',
119 'zh-CN': '火山引擎风险识别产品检测到输入文本可能包含敏感信息,请您更换后重试。',
120 },
121 'InputImageRiskDetection': {
122 'en-US': 'The request could not be processed because the input image includes sensitive content that violates ContentSecurityDetection.',
123 'zh-CN': '火山引擎风险识别产品检测到输入图片可能包含敏感信息,请您更换后重试。',
124 },
125 'OutputTextRiskDetection': {
126 'en-US': 'The request could not be processed because the output text includes sensitive content that violates ContentSecurityDetection.',
127 'zh-CN': '火山引擎风险识别产品检测到输出文本可能包含敏感信息,请您更换后重试。',
128 },
129 'OutputImageRiskDetection': {
130 'en-US': 'The request could not be processed because the output image includes sensitive content that violates ContentSecurityDetection.',
131 'zh-CN': '火山引擎风险识别产品检测到输出图片可能包含敏感信息,请您更换后重试。',
132 },
133 'ContentSecurityDetectionError': {
134 'en-US': 'Internal error.',
135 'zh-CN': '火山引擎风险识别产品请求失败。',
136 },
137 'InvalidArgumentError': {
138 'en-US': 'The request arguments are invalid.',
139 'zh-CN': '请求参数不合法,请检查消息结构后重试。',
140 },
141 'InvalidArgumentError.UnknownRole': {
142 'en-US': 'The role of message is not supported.',
143 'zh-CN': '消息体中的 role 不被支持,请检查参数后重试。',
144 },
145 'InvalidArgumentError.InvalidImageDetail': {
146 'en-US': 'Invalid image detail value.',
147 'zh-CN': 'image_url.detail 参数值无效,只支持 auto、high、low。',
148 },
149 'InvalidArgumentError.InvalidPixelLimit': {
150 'en-US': 'Customized image pixel limits are invalid.',
151 'zh-CN': '图片像素限制参数无效,请检查 min_pixels 与 max_pixels。',
152 },
153 'InvalidImageURL.EmptyURL': {
154 'en-US': 'Empty base64 image url.',
155 'zh-CN': '传入的图片 URL 为空。',
156 },
157 'InvalidImageURL.InvalidFormat': {
158 'en-US': 'Invalid base64 image url.',
159 'zh-CN': '图片 URL 或 Base64 数据无效,请检查格式后重试。',
160 },
161 'OutofContextError': {
162 'en-US': 'Total tokens of image and text exceed max message tokens.',
163 'zh-CN': '图文输入总 token 超出模型上下文长度限制,请缩短输入后重试。',
164 },
165 'AuthenticationError': {
166 'en-US': 'The API key or AK/SK in the request is missing or invalid.',
167 'zh-CN': '请求携带的 API Key 或 AK/SK 校验未通过,请检查鉴权凭证。',
168 },
169 'InvalidAccountStatus': {
170 'en-US': 'There is an issue with your account status.',
171 'zh-CN': '当前使用的账号状态异常,请联系平台管理员处理。',
172 },
173 'OperationDenied.ServiceNotOpen': {
174 'en-US': 'Operation is denied because the model service is unavailable.',
175 'zh-CN': '模型服务不可用,请前往火山方舟控制台开通对应模型服务。',
176 },
177 'OperationDenied.ServiceOverdue': {
178 'en-US': 'Operation is denied because your account balance is overdue.',
179 'zh-CN': '您的账单已逾期,请前往火山费用中心充值后重试。',
180 },
181 'AccountOverdueError': {
182 'en-US': 'The request failed because your account has an overdue balance.',
183 'zh-CN': '当前账号欠费,请充值后重试。',
184 },
185 'AccessDenied': {
186 'en-US': 'The request failed because you do not have access to the requested resource.',
187 'zh-CN': '没有访问该资源的权限,请检查权限设置或联系管理员。',
188 },
189 'InvalidEndpointOrModel.NotFound': {
190 'en-US': 'The model or endpoint does not exist or you do not have access to it.',
191 'zh-CN': '模型或推理接入点不存在,或者您无权访问该资源。',
192 },
193 'ModelNotOpen': {
194 'en-US': 'Your account has not activated the model service.',
195 'zh-CN': '当前账号暂未开通该模型服务,请前往火山方舟控制台开通。',
196 },
197 'InvalidEndpointOrModel.ModelIDAccessDisabled': {
198 'en-US': 'Accessing the model via Model ID is not allowed for your account.',
199 'zh-CN': '当前账号不允许通过模型 ID 调用,请改用有权限的推理接入点 ID。',
200 },
201 'RateLimitExceeded.EndpointRPMExceeded': {
202 'en-US': 'The Requests Per Minute limit of the associated endpoint for your account has been exceeded.',
203 'zh-CN': '请求已超过推理接入点 RPM 限制,请稍后重试。',
204 },
205 'RateLimitExceeded.EndpointTPMExceeded': {
206 'en-US': 'The Tokens Per Minute limit of the associated endpoint for your account has been exceeded.',
207 'zh-CN': '请求已超过推理接入点 TPM 限制,请稍后重试。',
208 },
209 'ModelAccountRpmRateLimitExceeded': {
210 'en-US': 'RPM limit of the model is exceeded.',
211 'zh-CN': '请求已超过账户模型 RPM 限制,请稍后重试。',
212 },
213 'ModelAccountTpmRateLimitExceeded': {
214 'en-US': 'TPM limit of the model is exceeded.',
215 'zh-CN': '请求已超过账户模型 TPM 限制,请稍后重试。',
216 },
217 'APIAccountRpmRateLimitExceeded': {
218 'en-US': 'The RPM limit for the API on your account has been exceeded.',
219 'zh-CN': '当前账号该接口的 RPM 限制已超出,请稍后重试。',
220 },
221 'ModelAccountIpmRateLimitExceeded': {
222 'en-US': 'IPM limit of the model is exceeded.',
223 'zh-CN': '请求已超过账户模型 IPM 限制,请稍后重试。',
224 },
225 'QuotaExceeded': {
226 'en-US': 'The request has exceeded the quota.',
227 'zh-CN': '当前账号额度已用尽或当前排队任务数超过限制,请稍后重试或检查额度配置。',
228 },
229 'ServerOverloaded': {
230 'en-US': 'The service is currently unable to handle additional requests due to server overload. Please retry later.',
231 'zh-CN': '服务资源紧张,请稍后重试。',
232 },
233 'RequestBurstTooFast': {
234 'en-US': 'System protection triggered by request burst. Please slow down traffic growth and increase requests gradually before retrying.',
235 'zh-CN': '请求量激增触发系统保护,请放缓流量提升速度后再试。',
236 },
237 'SetLimitExceeded': {
238 'en-US': 'Your account has reached the set inference limit for the model, and the model service has been paused.',
239 'zh-CN': '已达到当前模型设置的推理限额,请调整限额或关闭安心体验模式后重试。',
240 },
241 'InflightBatchsizeExceeded': {
242 'en-US': 'The Inflight Batchsize limit has been exceeded.',
243 'zh-CN': '已达到当前充值金额下的最大并发数限制,请降低并发或充值后重试。',
244 },
245 'AccountRateLimitExceeded': {
246 'en-US': 'Requests are too frequent. Please reduce your request frequency, wait a short moment, and retry your request.',
247 'zh-CN': '请求过于频繁,请稍后重试。',
248 },
249 'InternalServiceError': {
250 'en-US': 'The service encountered an unexpected internal error. Please retry later.',
251 'zh-CN': '内部系统异常,请稍后重试。',
252 },
253 }
254
255 const OFFICIAL_MESSAGE_BY_PREFIX: Array<{ prefix: string, message: OfficialMessage }> = [
256 {
257 prefix: 'MissingParameter.',
258 message: {
259 'en-US': 'The required parameter is missing.',
260 'zh-CN': '缺少必要的请求参数,请确认请求参数后重试。',
261 },
262 },
263 {
264 prefix: 'InvalidParameter.',
265 message: {
266 'en-US': 'The specified parameter is invalid.',
267 'zh-CN': '请求参数值不合法,请检查参数值的正确性后重试。',
268 },
269 },
270 ]
271
272 function extractResponseData(error: AxiosError): VolcengineErrorResponseData | undefined {
273 const data = error.response?.data
274 if (data == null || typeof data !== 'object') {
275 return undefined
276 }
277 return data as VolcengineErrorResponseData
278 }
279
280 function extractRequestId(rawMessage?: string): string | undefined {
281 if (!rawMessage) {
282 return undefined
283 }
284
285 const match = rawMessage.match(/(?:ARKRequest ID|Request ID)\s*[::]\s*([A-Z0-9-]+)/i)
286 return match?.[1]
287 }
288
289 function stripRequestIdFragment(rawMessage?: string): string | undefined {
290 if (!rawMessage) {
291 return undefined
292 }
293
294 return rawMessage
295 .replace(/\s*(?:ARKRequest ID|Request ID)\s*[::]\s*[A-Z0-9-]+(?:[;;].*)?$/i, '')
296 .trim()
297 }
298
299 function resolveOfficialMessage(code: string | number | undefined, locale: Locale): string | undefined {
300 if (code == null) {
301 return undefined
302 }
303
304 const normalizedCode = String(code)
305 const exactMessage = OFFICIAL_MESSAGE_BY_CODE[normalizedCode]
306 if (exactMessage) {
307 return exactMessage[locale]
308 }
309
310 const prefixMatch = OFFICIAL_MESSAGE_BY_PREFIX.find(item => normalizedCode.startsWith(item.prefix))
311 return prefixMatch?.message[locale]
312 }
313
314 function buildMessage(baseMessage: string, requestId?: string, locale: Locale = getLocale()): string {
315 if (!requestId) {
316 return baseMessage
317 }
318
319 return locale === 'zh-CN'
320 ? `${baseMessage}(Request ID: ${requestId})`
321 : `${baseMessage} (Request ID: ${requestId})`
322 }
323
324 function buildResponseBody(input: {
325 statusCode: number
326 message: string
327 operation: string
328 providerCode?: string | number
329 providerMessage?: string
330 providerType?: string
331 providerParam?: string
332 requestId?: string
333 }) {
334 const data: Record<string, unknown> = {
335 operation: input.operation,
336 }
337
338 if (input.providerCode !== undefined) {
339 data['providerCode'] = input.providerCode
340 }
341 if (input.providerMessage !== undefined) {
342 data['providerMessage'] = input.providerMessage
343 }
344 if (input.providerType !== undefined) {
345 data['providerType'] = input.providerType
346 }
347 if (input.providerParam !== undefined) {
348 data['providerParam'] = input.providerParam
349 }
350 if (input.requestId !== undefined) {
351 data['requestId'] = input.requestId
352 }
353
354 return {
355 code: input.statusCode,
356 message: input.message,
357 data,
358 }
359 }
360
361 export class VolcengineException extends HttpException {
362 readonly operation: string
363 readonly httpStatus: number
364 readonly providerCode?: string | number
365 readonly providerMessage?: string
366 readonly providerType?: string
367 readonly providerParam?: string
368 readonly requestId?: string
369 readonly raw?: unknown
370
371 constructor(input: VolcengineExceptionInput) {
372 const statusCode = input.httpStatus ?? HttpStatus.INTERNAL_SERVER_ERROR
373 const locale = getLocale()
374 const message = buildMessage(
375 resolveOfficialMessage(input.providerCode, locale)
376 ?? stripRequestIdFragment(input.providerMessage)
377 ?? DEFAULT_MESSAGE[locale],
378 input.requestId,
379 locale,
380 )
381
382 super(
383 buildResponseBody({
384 statusCode,
385 message,
386 operation: input.operation,
387 providerCode: input.providerCode,
388 providerMessage: input.providerMessage,
389 providerType: input.providerType,
390 providerParam: input.providerParam,
391 requestId: input.requestId,
392 }),
393 statusCode,
394 )
395
396 this.message = message
397 this.name = 'VolcengineException'
398 this.operation = input.operation
399 this.httpStatus = statusCode
400 this.providerCode = input.providerCode
401 this.providerMessage = input.providerMessage
402 this.providerType = input.providerType
403 this.providerParam = input.providerParam
404 this.requestId = input.requestId
405 this.raw = input.raw
406 }
407
408 static buildFromError(error: unknown, operation: string): VolcengineException {
409 if (error instanceof VolcengineException) {
410 return error
411 }
412
413 if (!isAxiosError(error)) {
414 return new VolcengineException({
415 operation,
416 providerMessage: getErrorMessage(error),
417 raw: error,
418 })
419 }
420
421 const responseData = extractResponseData(error)
422 const payload = responseData?.error
423
424 return new VolcengineException({
425 operation,
426 httpStatus: error.response?.status ?? HttpStatus.INTERNAL_SERVER_ERROR,
427 providerCode: payload?.code,
428 providerMessage: payload?.message ?? error.message,
429 providerType: payload?.type,
430 providerParam: payload?.param,
431 requestId: extractRequestId(payload?.message) ?? extractRequestId(error.message),
432 raw: error,
433 })
434 }
435 }
436
436 lines TYPESCRIPT